1 /*
   2  * Copyright (c) 2008, 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 com.sun.javafx.beans.event.AbstractNotifyListener;
  29 import com.sun.javafx.css.StyleManager;
  30 import com.sun.javafx.css.converters.URLConverter;
  31 import com.sun.javafx.geom.BaseBounds;
  32 import com.sun.javafx.geom.transform.BaseTransform;
  33 import com.sun.javafx.jmx.MXNodeAlgorithm;
  34 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
  35 import com.sun.javafx.scene.DirtyBits;
  36 import com.sun.javafx.sg.prism.NGImageView;
  37 import com.sun.javafx.sg.prism.NGNode;
  38 import com.sun.javafx.tk.Toolkit;
  39 import javafx.beans.DefaultProperty;
  40 import javafx.beans.Observable;
  41 import javafx.beans.property.*;
  42 import javafx.css.CssMetaData;
  43 import javafx.css.Styleable;
  44 import javafx.css.StyleableProperty;
  45 import javafx.css.StyleableStringProperty;
  46 import javafx.geometry.NodeOrientation;
  47 import javafx.geometry.Rectangle2D;
  48 import javafx.scene.AccessibleRole;
  49 import javafx.scene.Node;
  50 import java.util.ArrayList;
  51 import java.util.Collections;
  52 import java.util.List;
  53 
  54 /**
  55  * The {@code ImageView} is a {@code Node} used for painting images loaded with
  56  * {@link Image} class.
  57  *
  58  * <p>
  59  * This class allows resizing the displayed image (with or without preserving
  60  * the original aspect ratio) and specifying a viewport into the source image
  61  * for restricting the pixels displayed by this {@code ImageView}.
  62  * </p>
  63  *
  64  *
  65  * <p>
  66  * Example code for displaying images
  67  * </p>
  68  *
  69  * <pre>
  70  * <code>
  71  * import javafx.application.Application;
  72  * import javafx.geometry.Rectangle2D;
  73  * import javafx.scene.Group;
  74  * import javafx.scene.Scene; 
  75  * import javafx.scene.image.Image;
  76  * import javafx.scene.image.ImageView;
  77  * import javafx.scene.layout.HBox;
  78  * import javafx.scene.paint.Color;
  79  * import javafx.stage.Stage; 
  80  *
  81  * public class HelloMenu extends Application {
  82  * 
  83  *     &#64;Override public void start(Stage stage) {
  84  *         // load the image
  85  *         Image image = new Image("flower.png");
  86  * 
  87  *         // simple displays ImageView the image as is
  88  *         ImageView iv1 = new ImageView();
  89  *         iv1.setImage(image);
  90  * 
  91  *         // resizes the image to have width of 100 while preserving the ratio and using
  92  *         // higher quality filtering method; this ImageView is also cached to
  93  *         // improve performance
  94  *         ImageView iv2 = new ImageView();
  95  *         iv2.setImage(image);
  96  *         iv2.setFitWidth(100);
  97  *         iv2.setPreserveRatio(true);
  98  *         iv2.setSmooth(true);
  99  *         iv2.setCache(true);
 100  *
 101  *         // defines a viewport into the source image (achieving a "zoom" effect) and
 102  *         // displays it rotated
 103  *         ImageView iv3 = new ImageView();
 104  *         iv3.setImage(image);
 105  *         Rectangle2D viewportRect = new Rectangle2D(40, 35, 110, 110);
 106  *         iv3.setViewport(viewportRect);
 107  *         iv3.setRotate(90);
 108  *
 109  *         Group root = new Group();
 110  *         Scene scene = new Scene(root);
 111  *         scene.setFill(Color.BLACK);
 112  *         HBox box = new HBox();
 113  *         box.getChildren().add(iv1);
 114  *         box.getChildren().add(iv2);
 115  *         box.getChildren().add(iv3);
 116  *         root.getChildren().add(box);
 117  * 
 118  *         stage.setTitle("ImageView");
 119  *         stage.setWidth(415);
 120  *         stage.setHeight(200);
 121  *         stage.setScene(scene); 
 122  *         stage.sizeToScene(); 
 123  *         stage.show(); 
 124  *     }
 125  *
 126  *     public static void main(String[] args) {
 127  *         Application.launch(args);
 128  *     }
 129  * }
 130  * </code>
 131  * </pre>
 132  * <p>
 133  * The code above produces the following:
 134  * </p>
 135  * <p>
 136  * <img src="doc-files/imageview.png"/>
 137  * </p>
 138  * @since JavaFX 2.0
 139  */
 140 @DefaultProperty("image")
 141 public class ImageView extends Node {
 142 
 143     /**
 144      * Allocates a new ImageView object.
 145      */
 146     public ImageView() {
 147         getStyleClass().add(DEFAULT_STYLE_CLASS);
 148         setAccessibleRole(AccessibleRole.IMAGE_VIEW);
 149         setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
 150     }
 151 
 152     /**
 153      * Allocates a new ImageView object with image loaded from the specified
 154      * URL.
 155      * <p>
 156      * The {@code new ImageView(url)} has the same effect as
 157      * {@code new ImageView(new Image(url))}.
 158      * </p>
 159      *
 160      * @param url the string representing the URL from which to load the image
 161      * @throws NullPointerException if URL is null
 162      * @throws IllegalArgumentException if URL is invalid or unsupported
 163      * @since JavaFX 2.1
 164      */
 165     public ImageView(String url) {
 166         this(new Image(url));
 167     }
 168 
 169     /**
 170      * Allocates a new ImageView object using the given image.
 171      * 
 172      * @param image Image that this ImageView uses
 173      */
 174     public ImageView(Image image) {
 175         getStyleClass().add(DEFAULT_STYLE_CLASS);
 176         setAccessibleRole(AccessibleRole.IMAGE_VIEW);
 177         setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
 178         setImage(image);
 179     }
 180 
 181     /**
 182      * The {@link Image} to be painted by this {@code ImageView}.
 183      *
 184      * @defaultValue null
 185      */
 186     private ObjectProperty<Image> image;
 187 
 188     public final void setImage(Image value) {
 189         imageProperty().set(value);
 190     }
 191     public final Image getImage() {
 192         return image == null ? null : image.get();
 193     }
 194 
 195     private Image oldImage;
 196     public final ObjectProperty<Image> imageProperty() {
 197         if (image == null) {
 198             image = new ObjectPropertyBase<Image>() {
 199 
 200                 private boolean needsListeners = false;
 201 
 202                 @Override
 203                 public void invalidated() {
 204                     Image _image = get();
 205                     boolean dimensionChanged = _image == null || oldImage == null ||
 206                                                 (oldImage.getWidth() != _image.getWidth() ||
 207                                                 oldImage.getHeight() != _image.getHeight());
 208 
 209                     if (needsListeners) {
 210                         Toolkit.getImageAccessor().getImageProperty(oldImage).
 211                                 removeListener(platformImageChangeListener.getWeakListener());
 212                     }
 213 
 214                     needsListeners = _image != null && (_image.isAnimation() || _image.getProgress() < 1);
 215                     oldImage = _image;
 216 
 217                     if (needsListeners) {
 218                         Toolkit.getImageAccessor().getImageProperty(_image).
 219                                 addListener(platformImageChangeListener.getWeakListener());
 220                     }
 221                     if (dimensionChanged) {
 222                         invalidateWidthHeight();
 223                         impl_geomChanged();
 224                     }
 225                     impl_markDirty(DirtyBits.NODE_CONTENTS);
 226                 }
 227                 
 228                 @Override
 229                 public Object getBean() {
 230                     return ImageView.this;
 231                 }
 232 
 233                 @Override
 234                 public String getName() {
 235                     return "image";
 236                 }
 237             };
 238         }
 239         return image;
 240     }
 241 
 242     private StringProperty imageUrl = null;
 243     /**
 244      * The imageUrl property is set from CSS and then the image property is
 245      * set from the invalidated method. This ensures that the same image isn't
 246      * reloaded. 
 247      */
 248     private StringProperty imageUrlProperty() {
 249         if (imageUrl == null) {
 250             imageUrl = new StyleableStringProperty() {
 251 
 252                 @Override
 253                 protected void invalidated() {
 254 
 255                     final String imageUrl = get();
 256                     if (imageUrl != null) {
 257                         setImage(StyleManager.getInstance().getCachedImage(imageUrl)); 
 258                     } else {
 259                         setImage(null);
 260                     }                    
 261                 }
 262 
 263                 @Override
 264                 public Object getBean() {
 265                     return ImageView.this;
 266                 }
 267 
 268                 @Override
 269                 public String getName() {
 270                     return "imageUrl";
 271                 }
 272 
 273                 @Override
 274                 public CssMetaData<ImageView,String> getCssMetaData() {
 275                     return StyleableProperties.IMAGE;
 276                 }
 277                 
 278             };
 279         }
 280         return imageUrl;
 281     }    
 282 
 283     private final AbstractNotifyListener platformImageChangeListener =
 284             new AbstractNotifyListener() {
 285         @Override
 286         public void invalidated(Observable valueModel) {
 287             invalidateWidthHeight();
 288             impl_markDirty(DirtyBits.NODE_CONTENTS);
 289             impl_geomChanged();
 290         }
 291     };
 292     /**
 293      * The current x coordinate of the {@code ImageView} origin.
 294      *
 295      * @defaultValue 0
 296      */
 297     private DoubleProperty x;
 298 
 299 
 300     public final void setX(double value) {
 301         xProperty().set(value);
 302     }
 303 
 304     public final double getX() {
 305         return x == null ? 0.0 : x.get();
 306     }
 307 
 308     public final DoubleProperty xProperty() {
 309         if (x == null) {
 310             x = new DoublePropertyBase() {
 311 
 312                 @Override
 313                 protected void invalidated() {
 314                     impl_markDirty(DirtyBits.NODE_GEOMETRY);
 315                     impl_geomChanged();
 316                 }
 317 
 318                 @Override
 319                 public Object getBean() {
 320                     return ImageView.this;
 321                 }
 322 
 323                 @Override
 324                 public String getName() {
 325                     return "x";
 326                 }
 327             };
 328         }
 329         return x;
 330     }
 331 
 332     /**
 333      * The current y coordinate of the {@code ImageView} origin.
 334      *
 335      * @defaultValue 0
 336      */
 337     private DoubleProperty y;
 338 
 339 
 340     public final void setY(double value) {
 341         yProperty().set(value);
 342     }
 343 
 344     public final double getY() {
 345         return y == null ? 0.0 : y.get();
 346     }
 347 
 348     public final DoubleProperty yProperty() {
 349         if (y == null) {
 350             y = new DoublePropertyBase() {
 351 
 352                 @Override
 353                 protected void invalidated() {
 354                     impl_markDirty(DirtyBits.NODE_GEOMETRY);
 355                     impl_geomChanged();
 356                 }
 357 
 358                 @Override
 359                 public Object getBean() {
 360                     return ImageView.this;
 361                 }
 362 
 363                 @Override
 364                 public String getName() {
 365                     return "y";
 366                 }
 367             };
 368         }
 369         return y;
 370     }
 371 
 372     /**
 373      * The width of the bounding box within which the source image is resized as
 374      * necessary to fit. If set to a value <= 0, then the intrinsic width of the
 375      * image will be used as the {@code fitWidth}.
 376      * <p/>
 377      * See {@link #preserveRatio} for information on interaction between image
 378      * view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio}
 379      * attributes.
 380      *
 381      * @defaultValue 0
 382      */
 383     private DoubleProperty fitWidth;
 384 
 385 
 386     public final void setFitWidth(double value) {
 387         fitWidthProperty().set(value);
 388     }
 389 
 390     public final double getFitWidth() {
 391         return fitWidth == null ? 0.0 : fitWidth.get();
 392     }
 393 
 394     public final DoubleProperty fitWidthProperty() {
 395         if (fitWidth == null) {
 396             fitWidth = new DoublePropertyBase() {
 397 
 398                 @Override
 399                 protected void invalidated() {
 400                     invalidateWidthHeight();
 401                     impl_markDirty(DirtyBits.NODE_VIEWPORT);
 402                     impl_geomChanged();                    
 403                 }
 404 
 405                 @Override
 406                 public Object getBean() {
 407                     return ImageView.this;
 408                 }
 409 
 410                 @Override
 411                 public String getName() {
 412                     return "fitWidth";
 413                 }
 414             };
 415         }
 416         return fitWidth;
 417     }
 418 
 419     /**
 420      * The height of the bounding box within which the source image is resized
 421      * as necessary to fit. If set to a value <= 0, then the intrinsic height of
 422      * the image will be used as the {@code fitHeight}.
 423      * <p>
 424      * See {@link #preserveRatio} for information on interaction between image
 425      * view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio}
 426      * attributes.
 427      * </p>
 428      *
 429      * @defaultValue 0
 430      */
 431     private DoubleProperty fitHeight;
 432 
 433 
 434     public final void setFitHeight(double value) {
 435         fitHeightProperty().set(value);
 436     }
 437 
 438     public final double getFitHeight() {
 439         return fitHeight == null ? 0.0 : fitHeight.get();
 440     }
 441 
 442     public final DoubleProperty fitHeightProperty() {
 443         if (fitHeight == null) {
 444             fitHeight = new DoublePropertyBase() {
 445 
 446                 @Override
 447                 protected void invalidated() {
 448                     invalidateWidthHeight();
 449                     impl_markDirty(DirtyBits.NODE_VIEWPORT);
 450                     impl_geomChanged();
 451                 }
 452 
 453                 @Override
 454                 public Object getBean() {
 455                     return ImageView.this;
 456                 }
 457 
 458                 @Override
 459                 public String getName() {
 460                     return "fitHeight";
 461                 }
 462             };
 463         }
 464         return fitHeight;
 465     }
 466 
 467     /**
 468      * Indicates whether to preserve the aspect ratio of the source image when
 469      * scaling to fit the image within the fitting bounding box.
 470      * <p/>
 471      * If set to {@code true}, it affects the dimensions of this
 472      * {@code ImageView} in the following way *
 473      * <ul>
 474      * <li>If only {@code fitWidth} is set, height is scaled to preserve ratio
 475      * <li>If only {@code fitHeight} is set, width is scaled to preserve ratio
 476      * <li>If both are set, they both may be scaled to get the best fit in a
 477      * width by height rectangle while preserving the original aspect ratio
 478      * </ul>
 479      *
 480      * If unset or set to {@code false}, it affects the dimensions of this
 481      * {@code ImageView} in the following way *
 482      * <ul>
 483      * <li>If only {@code fitWidth} is set, image's view width is scaled to
 484      * match and height is unchanged;
 485      * <li>If only {@code fitHeight} is set, image's view height is scaled to
 486      * match and height is unchanged;
 487      * <li>If both are set, the image view is scaled to match both.
 488      * </ul>
 489      * </p>
 490      * Note that the dimensions of this node as reported by the node's bounds
 491      * will be equal to the size of the scaled image and is guaranteed to be
 492      * contained within {@code fitWidth x fitHeight} bonding box.
 493      *
 494      * @defaultValue false
 495      */
 496     private BooleanProperty preserveRatio;
 497 
 498 
 499     public final void setPreserveRatio(boolean value) {
 500         preserveRatioProperty().set(value);
 501     }
 502 
 503     public final boolean isPreserveRatio() {
 504         return preserveRatio == null ? false : preserveRatio.get();
 505     }
 506 
 507     public final BooleanProperty preserveRatioProperty() {
 508         if (preserveRatio == null) {
 509             preserveRatio = new BooleanPropertyBase() {
 510 
 511                 @Override
 512                 protected void invalidated() {
 513                     invalidateWidthHeight();
 514                     impl_markDirty(DirtyBits.NODE_VIEWPORT);
 515                     impl_geomChanged();
 516                 }
 517 
 518                 @Override
 519                 public Object getBean() {
 520                     return ImageView.this;
 521                 }
 522 
 523                 @Override
 524                 public String getName() {
 525                     return "preserveRatio";
 526                 }
 527             };
 528         }
 529         return preserveRatio;
 530     }
 531 
 532     /**
 533      * Indicates whether to use a better quality filtering algorithm or a faster
 534      * one when transforming or scaling the source image to fit within the
 535      * bounding box provided by {@code fitWidth} and {@code fitHeight}.
 536      *
 537      * <p>
 538      * If set to {@code true} a better quality filtering will be used, if set to
 539      * {@code false} a faster but lesser quality filtering will be used.
 540      * </p>
 541      *
 542      * <p>
 543      * The default value depends on platform configuration.
 544      * </p>
 545      *
 546      * @defaultValue platform-dependent
 547      */
 548     private BooleanProperty smooth;
 549 
 550 
 551     public final void setSmooth(boolean value) {
 552         smoothProperty().set(value);
 553     }
 554 
 555     public final boolean isSmooth() {
 556         return smooth == null ? SMOOTH_DEFAULT : smooth.get();
 557     }
 558 
 559     public final BooleanProperty smoothProperty() {
 560         if (smooth == null) {
 561             smooth = new BooleanPropertyBase(SMOOTH_DEFAULT) {
 562 
 563                 @Override
 564                 protected void invalidated() {
 565                     impl_markDirty(DirtyBits.NODE_SMOOTH);
 566                 }
 567 
 568                 @Override
 569                 public Object getBean() {
 570                     return ImageView.this;
 571                 }
 572 
 573                 @Override
 574                 public String getName() {
 575                     return "smooth";
 576                 }
 577             };
 578         }
 579         return smooth;
 580     }
 581 
 582     /**
 583      * Platform-dependent default value of the {@link #smoothProperty smooth} property.
 584      */
 585     public static final boolean SMOOTH_DEFAULT = Toolkit.getToolkit()
 586             .getDefaultImageSmooth();
 587     /**
 588      * The rectangular viewport into the image. The viewport is specified in the
 589      * coordinates of the image, prior to scaling or any other transformations.
 590      *
 591      * <p>
 592      * If {@code viewport} is {@code null}, the entire image is displayed. If
 593      * {@code viewport} is non-{@code null}, only the portion of the image which
 594      * falls within the viewport will be displayed. If the image does not fully
 595      * cover the viewport then any remaining area of the viewport will be empty.
 596      * </p>
 597      *
 598      * @defaultValue null
 599      */
 600     private ObjectProperty<Rectangle2D> viewport;
 601 
 602 
 603     public final void setViewport(Rectangle2D value) {
 604         viewportProperty().set(value);
 605     }
 606 
 607     public final Rectangle2D getViewport() {
 608         return viewport == null ? null : viewport.get();
 609     }
 610 
 611     public final ObjectProperty<Rectangle2D> viewportProperty() {
 612         if (viewport == null) {
 613             viewport = new ObjectPropertyBase<Rectangle2D>() {
 614 
 615                 @Override
 616                 protected void invalidated() {
 617                     invalidateWidthHeight();
 618                     impl_markDirty(DirtyBits.NODE_VIEWPORT);
 619                     impl_geomChanged();
 620                 }
 621 
 622                 @Override
 623                 public Object getBean() {
 624                     return ImageView.this;
 625                 }
 626 
 627                 @Override
 628                 public String getName() {
 629                     return "viewport";
 630                 }
 631             };
 632         }
 633         return viewport;
 634     }
 635 
 636     // Need to track changes to image width and image height and recompute
 637     // bounds when changed.
 638     // imageWidth = bind image.width on replace {
 639     // impl_geomChanged();
 640     // }
 641     //
 642     // imageHeight = bind image.height on replace {
 643     // impl_geomChanged();
 644     // }
 645 
 646     private double destWidth, destHeight;
 647 
 648     /**
 649      * @treatAsPrivate implementation detail
 650      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 651      */
 652     @Deprecated
 653     @Override protected NGNode impl_createPeer() {
 654         return new NGImageView();
 655     }
 656 
 657     /**
 658      * @treatAsPrivate implementation detail
 659      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 660      */
 661     @Deprecated
 662     @Override public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 663         recomputeWidthHeight();
 664 
 665         bounds = bounds.deriveWithNewBounds((float)getX(), (float)getY(), 0.0f,
 666                 (float)(getX() + destWidth), (float)(getY() + destHeight), 0.0f);
 667         bounds = tx.transform(bounds, bounds);
 668         return bounds;
 669     }
 670     
 671     private boolean validWH;
 672 
 673     private void invalidateWidthHeight() {
 674         validWH = false;
 675     }
 676 
 677     private void recomputeWidthHeight() {
 678         if (validWH) {
 679             return;
 680         }
 681         Image localImage = getImage();
 682         Rectangle2D localViewport = getViewport();
 683 
 684         double w = 0;
 685         double h = 0;
 686         if (localViewport != null && localViewport.getWidth() > 0 && localViewport.getHeight() > 0) {
 687             w = localViewport.getWidth();
 688             h = localViewport.getHeight();
 689         } else if (localImage != null) {
 690             w = localImage.getWidth();
 691             h = localImage.getHeight();
 692         }
 693 
 694         double localFitWidth = getFitWidth();
 695         double localFitHeight = getFitHeight();
 696 
 697         if (isPreserveRatio() && w > 0 && h > 0 && (localFitWidth > 0 || localFitHeight > 0)) {
 698             if (localFitWidth <= 0 || (localFitHeight > 0 && localFitWidth * h > localFitHeight * w)) {
 699                 w = w * localFitHeight / h;
 700                 h = localFitHeight;
 701             } else {
 702                 h = h * localFitWidth / w;
 703                 w = localFitWidth;
 704             }
 705         } else {
 706             if (localFitWidth > 0f) {
 707                 w = localFitWidth;
 708             }
 709             if (localFitHeight > 0f) {
 710                 h = localFitHeight;
 711             }
 712         }
 713 
 714         // Store these values for use later in impl_computeContains() to support
 715         // Node.contains().
 716         destWidth = w;
 717         destHeight = h;
 718 
 719         validWH = true;
 720     }
 721 
 722     /**
 723      * @treatAsPrivate implementation detail
 724      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 725      */
 726     @Deprecated
 727     @Override protected boolean impl_computeContains(double localX, double localY) {
 728         if (getImage() == null) {
 729             return false;
 730         }
 731 
 732         recomputeWidthHeight();
 733         // Local Note bounds contain test is already done by the caller.
 734         // (Node.contains()).
 735 
 736         double dx = localX - getX();
 737         double dy = localY - getY();
 738 
 739         Image localImage = getImage();
 740         double srcWidth = localImage.getWidth();
 741         double srcHeight = localImage.getHeight();
 742         double viewWidth = srcWidth;
 743         double viewHeight = srcHeight;
 744         double vw = 0;
 745         double vh = 0;
 746         double vminx = 0;
 747         double vminy = 0;
 748         Rectangle2D localViewport = getViewport();
 749         if (localViewport != null) {
 750             vw = localViewport.getWidth();
 751             vh = localViewport.getHeight();
 752             vminx = localViewport.getMinX();
 753             vminy = localViewport.getMinY();
 754         }
 755 
 756         if (vw > 0 && vh > 0) {
 757             viewWidth = vw;
 758             viewHeight = vh;
 759         }
 760 
 761         // desWidth Note and destHeight are computed by impl_computeGeomBounds()
 762         // via a call from Node.contains() before calling
 763         // impl_computeContains().
 764         // Transform into image's coordinate system.
 765         dx = vminx + dx * viewWidth / destWidth;
 766         dy = vminy + dy * viewHeight / destHeight;
 767         // test whether it's inside the original image AND inside of viewport
 768         // (viewport may stick out from the image bounds)
 769         if (dx < 0.0 || dy < 0.0 || dx >= srcWidth || dy >= srcHeight ||
 770                 dx < vminx || dy < vminy ||
 771                 dx >= vminx + viewWidth || dy >= vminy + viewHeight) {
 772             return false;
 773         }
 774         // Do alpha test on the picked pixel.
 775         return Toolkit.getToolkit().imageContains(localImage.impl_getPlatformImage(), (float)dx, (float)dy);
 776     }
 777 
 778     /***************************************************************************
 779      * * Stylesheet Handling * *
 780      **************************************************************************/
 781 
 782     private static final String DEFAULT_STYLE_CLASS = "image-view";
 783 
 784      /**
 785       * Super-lazy instantiation pattern from Bill Pugh.
 786       * @treatAsPrivate implementation detail
 787       */
 788      private static class StyleableProperties {
 789         // TODO
 790         // "preserve-ratio","smooth","viewport","fit-width","fit-height"
 791          private static final CssMetaData<ImageView, String> IMAGE = 
 792             new CssMetaData<ImageView,String>("-fx-image",
 793                 URLConverter.getInstance()) {
 794 
 795             @Override
 796             public boolean isSettable(ImageView n) {
 797                 // Note that we care about the image, not imageUrl
 798                 return n.image == null || !n.image.isBound();
 799             }
 800 
 801             @Override
 802             public StyleableProperty<String> getStyleableProperty(ImageView n) {
 803                 return (StyleableProperty<String>)n.imageUrlProperty();
 804             }
 805         };
 806             
 807          private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 808          static {
 809             final List<CssMetaData<? extends Styleable, ?>> styleables = 
 810                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Node.getClassCssMetaData());
 811             styleables.add(IMAGE);
 812             STYLEABLES = Collections.unmodifiableList(styleables);
 813          }
 814     }
 815 
 816     /**
 817      * @return The CssMetaData associated with this class, which may include the
 818      * CssMetaData of its super classes.
 819      * @since JavaFX 8.0
 820      */
 821     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 822         return StyleableProperties.STYLEABLES;
 823     }
 824 
 825     /**
 826      * {@inheritDoc}
 827      *
 828      * @since JavaFX 8.0
 829      */
 830     
 831     
 832     @Override
 833     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 834         return getClassCssMetaData();
 835     }
 836 
 837     void updateViewport() {
 838         recomputeWidthHeight();
 839         if (getImage() == null || getImage().impl_getPlatformImage() == null) {
 840             return;
 841         }
 842 
 843         Rectangle2D localViewport = getViewport();
 844         final NGImageView peer = impl_getPeer();
 845         if (localViewport != null) {
 846             peer.setViewport((float)localViewport.getMinX(), (float)localViewport.getMinY(),
 847                     (float)localViewport.getWidth(), (float)localViewport.getHeight(),
 848                     (float)destWidth, (float)destHeight);
 849         } else {
 850             peer.setViewport(0, 0, 0, 0, (float)destWidth, (float)destHeight);
 851         }
 852     }
 853 
 854     /**
 855      * @treatAsPrivate implementation detail
 856      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 857      */
 858     @Deprecated
 859     @Override public void impl_updatePeer() {
 860         super.impl_updatePeer();
 861 
 862         final NGImageView peer = impl_getPeer();
 863         if (impl_isDirty(DirtyBits.NODE_GEOMETRY)) {
 864             peer.setX((float)getX());
 865             peer.setY((float)getY());
 866         }
 867         if (impl_isDirty(DirtyBits.NODE_SMOOTH)) {
 868             peer.setSmooth(isSmooth());
 869         }
 870         if (impl_isDirty(DirtyBits.NODE_CONTENTS)) {
 871             peer.setImage(getImage()!= null? getImage().impl_getPlatformImage():null);
 872         }
 873         // The NG part expects this to be called when image changes
 874         if (impl_isDirty(DirtyBits.NODE_VIEWPORT) || impl_isDirty(DirtyBits.NODE_CONTENTS)) {
 875             updateViewport();
 876         }
 877     }
 878 
 879     /**
 880      * @treatAsPrivate implementation detail
 881      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 882      */
 883     @Deprecated
 884     @Override public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
 885         return alg.processLeafNode(this, ctx);
 886     }
 887 }