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>(> 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}