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