/* * Copyright (c) 2008, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.image; import com.sun.javafx.beans.event.AbstractNotifyListener; import com.sun.javafx.css.StyleManager; import javafx.css.converter.URLConverter; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.transform.BaseTransform; import com.sun.javafx.jmx.MXNodeAlgorithm; import com.sun.javafx.jmx.MXNodeAlgorithmContext; import com.sun.javafx.scene.DirtyBits; import com.sun.javafx.scene.ImageViewHelper; import com.sun.javafx.scene.NodeHelper; import com.sun.javafx.sg.prism.NGImageView; import com.sun.javafx.sg.prism.NGNode; import com.sun.javafx.tk.Toolkit; import javafx.beans.DefaultProperty; import javafx.beans.Observable; import javafx.beans.property.*; import javafx.css.CssMetaData; import javafx.css.Styleable; import javafx.css.StyleableProperty; import javafx.css.StyleableStringProperty; import javafx.geometry.NodeOrientation; import javafx.geometry.Rectangle2D; import javafx.scene.AccessibleRole; import javafx.scene.Node; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * The {@code ImageView} is a {@code Node} used for painting images loaded with * {@link Image} class. * *

* This class allows resizing the displayed image (with or without preserving * the original aspect ratio) and specifying a viewport into the source image * for restricting the pixels displayed by this {@code ImageView}. *

* * *

* Example code for displaying images *

* *
 * 
 * import javafx.application.Application;
 * import javafx.geometry.Rectangle2D;
 * import javafx.scene.Group;
 * import javafx.scene.Scene;
 * import javafx.scene.image.Image;
 * import javafx.scene.image.ImageView;
 * import javafx.scene.layout.HBox;
 * import javafx.scene.paint.Color;
 * import javafx.stage.Stage;
 *
 * public class HelloMenu extends Application {
 *
 *     @Override public void start(Stage stage) {
 *         // load the image
 *         Image image = new Image("flower.png");
 *
 *         // simple displays ImageView the image as is
 *         ImageView iv1 = new ImageView();
 *         iv1.setImage(image);
 *
 *         // resizes the image to have width of 100 while preserving the ratio and using
 *         // higher quality filtering method; this ImageView is also cached to
 *         // improve performance
 *         ImageView iv2 = new ImageView();
 *         iv2.setImage(image);
 *         iv2.setFitWidth(100);
 *         iv2.setPreserveRatio(true);
 *         iv2.setSmooth(true);
 *         iv2.setCache(true);
 *
 *         // defines a viewport into the source image (achieving a "zoom" effect) and
 *         // displays it rotated
 *         ImageView iv3 = new ImageView();
 *         iv3.setImage(image);
 *         Rectangle2D viewportRect = new Rectangle2D(40, 35, 110, 110);
 *         iv3.setViewport(viewportRect);
 *         iv3.setRotate(90);
 *
 *         Group root = new Group();
 *         Scene scene = new Scene(root);
 *         scene.setFill(Color.BLACK);
 *         HBox box = new HBox();
 *         box.getChildren().add(iv1);
 *         box.getChildren().add(iv2);
 *         box.getChildren().add(iv3);
 *         root.getChildren().add(box);
 *
 *         stage.setTitle("ImageView");
 *         stage.setWidth(415);
 *         stage.setHeight(200);
 *         stage.setScene(scene);
 *         stage.sizeToScene();
 *         stage.show();
 *     }
 *
 *     public static void main(String[] args) {
 *         Application.launch(args);
 *     }
 * }
 * 
 * 
*

* The code above produces the following: *

*

* *

* @since JavaFX 2.0 */ @DefaultProperty("image") public class ImageView extends Node { static { // This is used by classes in different packages to get access to // private and package private methods. ImageViewHelper.setImageViewAccessor(new ImageViewHelper.ImageViewAccessor() { @Override public NGNode doCreatePeer(Node node) { return ((ImageView) node).doCreatePeer(); } @Override public void doUpdatePeer(Node node) { ((ImageView) node).doUpdatePeer(); } @Override public BaseBounds doComputeGeomBounds(Node node, BaseBounds bounds, BaseTransform tx) { return ((ImageView) node).doComputeGeomBounds(bounds, tx); } @Override public boolean doComputeContains(Node node, double localX, double localY) { return ((ImageView) node).doComputeContains(localX, localY); } @Override public Object doProcessMXNode(Node node, MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) { return ((ImageView) node).doProcessMXNode(alg, ctx); } }); } { // To initialize the class helper at the begining each constructor of this class ImageViewHelper.initHelper(this); } /** * Allocates a new ImageView object. */ public ImageView() { getStyleClass().add(DEFAULT_STYLE_CLASS); setAccessibleRole(AccessibleRole.IMAGE_VIEW); setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); } /** * Allocates a new ImageView object with image loaded from the specified * URL. *

* The {@code new ImageView(url)} has the same effect as * {@code new ImageView(new Image(url))}. *

* * @param url the string representing the URL from which to load the image * @throws NullPointerException if URL is null * @throws IllegalArgumentException if URL is invalid or unsupported * @since JavaFX 2.1 */ public ImageView(String url) { this(new Image(url)); } /** * Allocates a new ImageView object using the given image. * * @param image Image that this ImageView uses */ public ImageView(Image image) { getStyleClass().add(DEFAULT_STYLE_CLASS); setAccessibleRole(AccessibleRole.IMAGE_VIEW); setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); setImage(image); } /** * The {@link Image} to be painted by this {@code ImageView}. * * @defaultValue null */ private ObjectProperty image; public final void setImage(Image value) { imageProperty().set(value); } public final Image getImage() { return image == null ? null : image.get(); } private Image oldImage; public final ObjectProperty imageProperty() { if (image == null) { image = new ObjectPropertyBase() { private boolean needsListeners = false; @Override public void invalidated() { Image _image = get(); boolean dimensionChanged = _image == null || oldImage == null || (oldImage.getWidth() != _image.getWidth() || oldImage.getHeight() != _image.getHeight()); if (needsListeners) { Toolkit.getImageAccessor().getImageProperty(oldImage). removeListener(platformImageChangeListener.getWeakListener()); } needsListeners = _image != null && (_image.isAnimation() || _image.getProgress() < 1); oldImage = _image; if (needsListeners) { Toolkit.getImageAccessor().getImageProperty(_image). addListener(platformImageChangeListener.getWeakListener()); } if (dimensionChanged) { invalidateWidthHeight(); NodeHelper.geomChanged(ImageView.this); } NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_CONTENTS); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "image"; } }; } return image; } private StringProperty imageUrl = null; /** * The imageUrl property is set from CSS and then the image property is * set from the invalidated method. This ensures that the same image isn't * reloaded. */ private StringProperty imageUrlProperty() { if (imageUrl == null) { imageUrl = new StyleableStringProperty() { @Override protected void invalidated() { final String imageUrl = get(); if (imageUrl != null) { setImage(StyleManager.getInstance().getCachedImage(imageUrl)); } else { setImage(null); } } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "imageUrl"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.IMAGE; } }; } return imageUrl; } private final AbstractNotifyListener platformImageChangeListener = new AbstractNotifyListener() { @Override public void invalidated(Observable valueModel) { invalidateWidthHeight(); NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_CONTENTS); NodeHelper.geomChanged(ImageView.this); } }; /** * The current x coordinate of the {@code ImageView} origin. * * @defaultValue 0 */ private DoubleProperty x; public final void setX(double value) { xProperty().set(value); } public final double getX() { return x == null ? 0.0 : x.get(); } public final DoubleProperty xProperty() { if (x == null) { x = new DoublePropertyBase() { @Override protected void invalidated() { NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_GEOMETRY); NodeHelper.geomChanged(ImageView.this); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "x"; } }; } return x; } /** * The current y coordinate of the {@code ImageView} origin. * * @defaultValue 0 */ private DoubleProperty y; public final void setY(double value) { yProperty().set(value); } public final double getY() { return y == null ? 0.0 : y.get(); } public final DoubleProperty yProperty() { if (y == null) { y = new DoublePropertyBase() { @Override protected void invalidated() { NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_GEOMETRY); NodeHelper.geomChanged(ImageView.this); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "y"; } }; } return y; } /** * The width of the bounding box within which the source image is resized as * necessary to fit. If set to a value <= 0, then the intrinsic width of the * image will be used as the {@code fitWidth}. *

* See {@link #preserveRatio} for information on interaction between image * view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio} * attributes. * * @defaultValue 0 */ private DoubleProperty fitWidth; public final void setFitWidth(double value) { fitWidthProperty().set(value); } public final double getFitWidth() { return fitWidth == null ? 0.0 : fitWidth.get(); } public final DoubleProperty fitWidthProperty() { if (fitWidth == null) { fitWidth = new DoublePropertyBase() { @Override protected void invalidated() { invalidateWidthHeight(); NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); NodeHelper.geomChanged(ImageView.this); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "fitWidth"; } }; } return fitWidth; } /** * The height of the bounding box within which the source image is resized * as necessary to fit. If set to a value <= 0, then the intrinsic height of * the image will be used as the {@code fitHeight}. *

* See {@link #preserveRatio} for information on interaction between image * view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio} * attributes. *

* * @defaultValue 0 */ private DoubleProperty fitHeight; public final void setFitHeight(double value) { fitHeightProperty().set(value); } public final double getFitHeight() { return fitHeight == null ? 0.0 : fitHeight.get(); } public final DoubleProperty fitHeightProperty() { if (fitHeight == null) { fitHeight = new DoublePropertyBase() { @Override protected void invalidated() { invalidateWidthHeight(); NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); NodeHelper.geomChanged(ImageView.this); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "fitHeight"; } }; } return fitHeight; } /** * Indicates whether to preserve the aspect ratio of the source image when * scaling to fit the image within the fitting bounding box. *

* If set to {@code true}, it affects the dimensions of this * {@code ImageView} in the following way * *

* * If unset or set to {@code false}, it affects the dimensions of this * {@code ImageView} in the following way * * *

* Note that the dimensions of this node as reported by the node's bounds * will be equal to the size of the scaled image and is guaranteed to be * contained within {@code fitWidth x fitHeight} bonding box. * * @defaultValue false */ private BooleanProperty preserveRatio; public final void setPreserveRatio(boolean value) { preserveRatioProperty().set(value); } public final boolean isPreserveRatio() { return preserveRatio == null ? false : preserveRatio.get(); } public final BooleanProperty preserveRatioProperty() { if (preserveRatio == null) { preserveRatio = new BooleanPropertyBase() { @Override protected void invalidated() { invalidateWidthHeight(); NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); NodeHelper.geomChanged(ImageView.this); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "preserveRatio"; } }; } return preserveRatio; } /** * Indicates whether to use a better quality filtering algorithm or a faster * one when transforming or scaling the source image to fit within the * bounding box provided by {@code fitWidth} and {@code fitHeight}. * *

* If set to {@code true} a better quality filtering will be used, if set to * {@code false} a faster but lesser quality filtering will be used. *

* *

* The default value depends on platform configuration. *

* * @defaultValue platform-dependent */ private BooleanProperty smooth; public final void setSmooth(boolean value) { smoothProperty().set(value); } public final boolean isSmooth() { return smooth == null ? SMOOTH_DEFAULT : smooth.get(); } public final BooleanProperty smoothProperty() { if (smooth == null) { smooth = new BooleanPropertyBase(SMOOTH_DEFAULT) { @Override protected void invalidated() { NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_SMOOTH); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "smooth"; } }; } return smooth; } /** * Platform-dependent default value of the {@link #smoothProperty smooth} property. */ public static final boolean SMOOTH_DEFAULT = Toolkit.getToolkit() .getDefaultImageSmooth(); /** * The rectangular viewport into the image. The viewport is specified in the * coordinates of the image, prior to scaling or any other transformations. * *

* If {@code viewport} is {@code null}, the entire image is displayed. If * {@code viewport} is non-{@code null}, only the portion of the image which * falls within the viewport will be displayed. If the image does not fully * cover the viewport then any remaining area of the viewport will be empty. *

* * @defaultValue null */ private ObjectProperty viewport; public final void setViewport(Rectangle2D value) { viewportProperty().set(value); } public final Rectangle2D getViewport() { return viewport == null ? null : viewport.get(); } public final ObjectProperty viewportProperty() { if (viewport == null) { viewport = new ObjectPropertyBase() { @Override protected void invalidated() { invalidateWidthHeight(); NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); NodeHelper.geomChanged(ImageView.this); } @Override public Object getBean() { return ImageView.this; } @Override public String getName() { return "viewport"; } }; } return viewport; } // Need to track changes to image width and image height and recompute // bounds when changed. // imageWidth = bind image.width on replace { // NodeHelper.geomChanged(ImageView.this); // } // // imageHeight = bind image.height on replace { // NodeHelper.geomChanged(ImageView.this); // } private double destWidth, destHeight; /* * Note: This method MUST only be called via its accessor method. */ private NGNode doCreatePeer() { return new NGImageView(); } /* * Note: This method MUST only be called via its accessor method. */ private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { recomputeWidthHeight(); bounds = bounds.deriveWithNewBounds((float)getX(), (float)getY(), 0.0f, (float)(getX() + destWidth), (float)(getY() + destHeight), 0.0f); bounds = tx.transform(bounds, bounds); return bounds; } private boolean validWH; private void invalidateWidthHeight() { validWH = false; } private void recomputeWidthHeight() { if (validWH) { return; } Image localImage = getImage(); Rectangle2D localViewport = getViewport(); double w = 0; double h = 0; if (localViewport != null && localViewport.getWidth() > 0 && localViewport.getHeight() > 0) { w = localViewport.getWidth(); h = localViewport.getHeight(); } else if (localImage != null) { w = localImage.getWidth(); h = localImage.getHeight(); } double localFitWidth = getFitWidth(); double localFitHeight = getFitHeight(); if (isPreserveRatio() && w > 0 && h > 0 && (localFitWidth > 0 || localFitHeight > 0)) { if (localFitWidth <= 0 || (localFitHeight > 0 && localFitWidth * h > localFitHeight * w)) { w = w * localFitHeight / h; h = localFitHeight; } else { h = h * localFitWidth / w; w = localFitWidth; } } else { if (localFitWidth > 0f) { w = localFitWidth; } if (localFitHeight > 0f) { h = localFitHeight; } } // Store these values for use later in doComputeContains() to support // Node.contains(). destWidth = w; destHeight = h; validWH = true; } /* * Note: This method MUST only be called via its accessor method. */ private boolean doComputeContains(double localX, double localY) { if (getImage() == null) { return false; } recomputeWidthHeight(); // Local Note bounds contain test is already done by the caller. // (Node.contains()). double dx = localX - getX(); double dy = localY - getY(); Image localImage = getImage(); double srcWidth = localImage.getWidth(); double srcHeight = localImage.getHeight(); double viewWidth = srcWidth; double viewHeight = srcHeight; double vw = 0; double vh = 0; double vminx = 0; double vminy = 0; Rectangle2D localViewport = getViewport(); if (localViewport != null) { vw = localViewport.getWidth(); vh = localViewport.getHeight(); vminx = localViewport.getMinX(); vminy = localViewport.getMinY(); } if (vw > 0 && vh > 0) { viewWidth = vw; viewHeight = vh; } // desWidth Note and destHeight are computed by NodeHelper.computeGeomBounds() // via a call from Node.contains() before calling // doComputeContains(). // Transform into image's coordinate system. dx = vminx + dx * viewWidth / destWidth; dy = vminy + dy * viewHeight / destHeight; // test whether it's inside the original image AND inside of viewport // (viewport may stick out from the image bounds) if (dx < 0.0 || dy < 0.0 || dx >= srcWidth || dy >= srcHeight || dx < vminx || dy < vminy || dx >= vminx + viewWidth || dy >= vminy + viewHeight) { return false; } // Do alpha test on the picked pixel. return Toolkit.getToolkit().imageContains( Toolkit.getImageAccessor().getPlatformImage(localImage), (float)dx, (float)dy); } /*************************************************************************** * * Stylesheet Handling * * **************************************************************************/ private static final String DEFAULT_STYLE_CLASS = "image-view"; /* * Super-lazy instantiation pattern from Bill Pugh. */ private static class StyleableProperties { // TODO // "preserve-ratio","smooth","viewport","fit-width","fit-height" private static final CssMetaData IMAGE = new CssMetaData("-fx-image", URLConverter.getInstance()) { @Override public boolean isSettable(ImageView n) { // Note that we care about the image, not imageUrl return n.image == null || !n.image.isBound(); } @Override public StyleableProperty getStyleableProperty(ImageView n) { return (StyleableProperty)n.imageUrlProperty(); } }; private static final List> STYLEABLES; static { final List> styleables = new ArrayList>(Node.getClassCssMetaData()); styleables.add(IMAGE); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its super classes. * @since JavaFX 8.0 */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** * {@inheritDoc} * * @since JavaFX 8.0 */ @Override public List> getCssMetaData() { return getClassCssMetaData(); } void updateViewport() { recomputeWidthHeight(); if (getImage() == null || Toolkit.getImageAccessor().getPlatformImage(getImage()) == null) { return; } Rectangle2D localViewport = getViewport(); final NGImageView peer = NodeHelper.getPeer(this); if (localViewport != null) { peer.setViewport((float)localViewport.getMinX(), (float)localViewport.getMinY(), (float)localViewport.getWidth(), (float)localViewport.getHeight(), (float)destWidth, (float)destHeight); } else { peer.setViewport(0, 0, 0, 0, (float)destWidth, (float)destHeight); } } /* * Note: This method MUST only be called via its accessor method. */ private void doUpdatePeer() { final NGImageView peer = NodeHelper.getPeer(this); if (NodeHelper.isDirty(this, DirtyBits.NODE_GEOMETRY)) { peer.setX((float)getX()); peer.setY((float)getY()); } if (NodeHelper.isDirty(this, DirtyBits.NODE_SMOOTH)) { peer.setSmooth(isSmooth()); } if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) { peer.setImage(getImage() != null ? Toolkit.getImageAccessor().getPlatformImage(getImage()) : null); } // The NG part expects this to be called when image changes if (NodeHelper.isDirty(this, DirtyBits.NODE_VIEWPORT) || NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) { updateViewport(); } } /* * Note: This method MUST only be called via its accessor method. */ private Object doProcessMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) { return alg.processLeafNode(this, ctx); } }