1 /*
   2  * Copyright (c) 1997, 2013, 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 package javax.swing.text.html;
  26 
  27 import java.awt.*;
  28 import java.awt.image.ImageObserver;
  29 import java.net.*;
  30 import java.util.Dictionary;
  31 import javax.swing.*;
  32 import javax.swing.text.*;
  33 import javax.swing.event.*;
  34 
  35 /**
  36  * View of an Image, intended to support the HTML <IMG> tag.
  37  * Supports scaling via the HEIGHT and WIDTH attributes of the tag.
  38  * If the image is unable to be loaded any text specified via the
  39  * <code>ALT</code> attribute will be rendered.
  40  * <p>
  41  * While this class has been part of swing for a while now, it is public
  42  * as of 1.4.
  43  *
  44  * @author  Scott Violet
  45  * @see IconView
  46  * @since 1.4
  47  */
  48 public class ImageView extends View {
  49     /**
  50      * If true, when some of the bits are available a repaint is done.
  51      * <p>
  52      * This is set to false as swing does not offer a repaint that takes a
  53      * delay. If this were true, a bunch of immediate repaints would get
  54      * generated that end up significantly delaying the loading of the image
  55      * (or anything else going on for that matter).
  56      */
  57     private static boolean sIsInc = false;
  58     /**
  59      * Repaint delay when some of the bits are available.
  60      */
  61     private static int sIncRate = 100;
  62     /**
  63      * Property name for pending image icon
  64      */
  65     private static final String PENDING_IMAGE = "html.pendingImage";
  66     /**
  67      * Property name for missing image icon
  68      */
  69     private static final String MISSING_IMAGE = "html.missingImage";
  70 
  71     /**
  72      * Document property for image cache.
  73      */
  74     private static final String IMAGE_CACHE_PROPERTY = "imageCache";
  75 
  76     // Height/width to use before we know the real size, these should at least
  77     // the size of <code>sMissingImageIcon</code> and
  78     // <code>sPendingImageIcon</code>
  79     private static final int DEFAULT_WIDTH = 38;
  80     private static final int DEFAULT_HEIGHT= 38;
  81 
  82     /**
  83      * Default border to use if one is not specified.
  84      */
  85     private static final int DEFAULT_BORDER = 2;
  86 
  87     // Bitmask values
  88     private static final int LOADING_FLAG = 1;
  89     private static final int LINK_FLAG = 2;
  90     private static final int WIDTH_FLAG = 4;
  91     private static final int HEIGHT_FLAG = 8;
  92     private static final int RELOAD_FLAG = 16;
  93     private static final int RELOAD_IMAGE_FLAG = 32;
  94     private static final int SYNC_LOAD_FLAG = 64;
  95 
  96     private AttributeSet attr;
  97     private Image image;
  98     private Image disabledImage;
  99     private int width;
 100     private int height;
 101     /** Bitmask containing some of the above bitmask values. Because the
 102      * image loading notification can happen on another thread access to
 103      * this is synchronized (at least for modifying it). */
 104     private int state;
 105     private Container container;
 106     private Rectangle fBounds;
 107     private Color borderColor;
 108     // Size of the border, the insets contains this valid. For example, if
 109     // the HSPACE attribute was 4 and BORDER 2, leftInset would be 6.
 110     private short borderSize;
 111     // Insets, obtained from the painter.
 112     private short leftInset;
 113     private short rightInset;
 114     private short topInset;
 115     private short bottomInset;
 116     /**
 117      * We don't directly implement ImageObserver, instead we use an instance
 118      * that calls back to us.
 119      */
 120     private ImageObserver imageObserver;
 121     /**
 122      * Used for alt text. Will be non-null if the image couldn't be found,
 123      * and there is valid alt text.
 124      */
 125     private View altView;
 126     /** Alignment along the vertical (Y) axis. */
 127     private float vAlign;
 128 
 129 
 130 
 131     /**
 132      * Creates a new view that represents an IMG element.
 133      *
 134      * @param elem the element to create a view for
 135      */
 136     public ImageView(Element elem) {
 137         super(elem);
 138         fBounds = new Rectangle();
 139         imageObserver = new ImageHandler();
 140         state = RELOAD_FLAG | RELOAD_IMAGE_FLAG;
 141     }
 142 
 143     /**
 144      * Returns the text to display if the image can't be loaded. This is
 145      * obtained from the Elements attribute set with the attribute name
 146      * <code>HTML.Attribute.ALT</code>.
 147      */
 148     public String getAltText() {
 149         return (String)getElement().getAttributes().getAttribute
 150             (HTML.Attribute.ALT);
 151     }
 152 
 153     /**
 154      * Return a URL for the image source,
 155      * or null if it could not be determined.
 156      */
 157     public URL getImageURL() {
 158         String src = (String)getElement().getAttributes().
 159                              getAttribute(HTML.Attribute.SRC);
 160         if (src == null) {
 161             return null;
 162         }
 163 
 164         URL reference = ((HTMLDocument)getDocument()).getBase();
 165         try {
 166             URL u = new URL(reference,src);
 167             return u;
 168         } catch (MalformedURLException e) {
 169             return null;
 170         }
 171     }
 172 
 173     /**
 174      * Returns the icon to use if the image couldn't be found.
 175      */
 176     public Icon getNoImageIcon() {
 177         return (Icon) UIManager.getLookAndFeelDefaults().get(MISSING_IMAGE);
 178     }
 179 
 180     /**
 181      * Returns the icon to use while in the process of loading the image.
 182      */
 183     public Icon getLoadingImageIcon() {
 184         return (Icon) UIManager.getLookAndFeelDefaults().get(PENDING_IMAGE);
 185     }
 186 
 187     /**
 188      * Returns the image to render.
 189      */
 190     public Image getImage() {
 191         sync();
 192         return image;
 193     }
 194 
 195     private Image getImage(boolean enabled) {
 196         Image img = getImage();
 197         if (! enabled) {
 198             if (disabledImage == null) {
 199                 disabledImage = GrayFilter.createDisabledImage(img);
 200             }
 201             img = disabledImage;
 202         }
 203         return img;
 204     }
 205 
 206     /**
 207      * Sets how the image is loaded. If <code>newValue</code> is true,
 208      * the image we be loaded when first asked for, otherwise it will
 209      * be loaded asynchronously. The default is to not load synchronously,
 210      * that is to load the image asynchronously.
 211      */
 212     public void setLoadsSynchronously(boolean newValue) {
 213         synchronized(this) {
 214             if (newValue) {
 215                 state |= SYNC_LOAD_FLAG;
 216             }
 217             else {
 218                 state = (state | SYNC_LOAD_FLAG) ^ SYNC_LOAD_FLAG;
 219             }
 220         }
 221     }
 222 
 223     /**
 224      * Returns true if the image should be loaded when first asked for.
 225      */
 226     public boolean getLoadsSynchronously() {
 227         return ((state & SYNC_LOAD_FLAG) != 0);
 228     }
 229 
 230     /**
 231      * Convenience method to get the StyleSheet.
 232      */
 233     protected StyleSheet getStyleSheet() {
 234         HTMLDocument doc = (HTMLDocument) getDocument();
 235         return doc.getStyleSheet();
 236     }
 237 
 238     /**
 239      * Fetches the attributes to use when rendering.  This is
 240      * implemented to multiplex the attributes specified in the
 241      * model with a StyleSheet.
 242      */
 243     public AttributeSet getAttributes() {
 244         sync();
 245         return attr;
 246     }
 247 
 248     /**
 249      * For images the tooltip text comes from text specified with the
 250      * <code>ALT</code> attribute. This is overriden to return
 251      * <code>getAltText</code>.
 252      *
 253      * @see JTextComponent#getToolTipText
 254      */
 255     public String getToolTipText(float x, float y, Shape allocation) {
 256         return getAltText();
 257     }
 258 
 259     /**
 260      * Update any cached values that come from attributes.
 261      */
 262     protected void setPropertiesFromAttributes() {
 263         StyleSheet sheet = getStyleSheet();
 264         this.attr = sheet.getViewAttributes(this);
 265 
 266         // Gutters
 267         borderSize = (short)getIntAttr(HTML.Attribute.BORDER, isLink() ?
 268                                        DEFAULT_BORDER : 0);
 269 
 270         leftInset = rightInset = (short)(getIntAttr(HTML.Attribute.HSPACE,
 271                                                     0) + borderSize);
 272         topInset = bottomInset = (short)(getIntAttr(HTML.Attribute.VSPACE,
 273                                                     0) + borderSize);
 274 
 275         borderColor = ((StyledDocument)getDocument()).getForeground
 276                       (getAttributes());
 277 
 278         AttributeSet attr = getElement().getAttributes();
 279 
 280         // Alignment.
 281         // PENDING: This needs to be changed to support the CSS versions
 282         // when conversion from ALIGN to VERTICAL_ALIGN is complete.
 283         Object alignment = attr.getAttribute(HTML.Attribute.ALIGN);
 284 
 285         vAlign = 1.0f;
 286         if (alignment != null) {
 287             alignment = alignment.toString();
 288             if ("top".equals(alignment)) {
 289                 vAlign = 0f;
 290             }
 291             else if ("middle".equals(alignment)) {
 292                 vAlign = .5f;
 293             }
 294         }
 295 
 296         AttributeSet anchorAttr = (AttributeSet)attr.getAttribute(HTML.Tag.A);
 297         if (anchorAttr != null && anchorAttr.isDefined
 298             (HTML.Attribute.HREF)) {
 299             synchronized(this) {
 300                 state |= LINK_FLAG;
 301             }
 302         }
 303         else {
 304             synchronized(this) {
 305                 state = (state | LINK_FLAG) ^ LINK_FLAG;
 306             }
 307         }
 308     }
 309 
 310     /**
 311      * Establishes the parent view for this view.
 312      * Seize this moment to cache the AWT Container I'm in.
 313      */
 314     public void setParent(View parent) {
 315         View oldParent = getParent();
 316         super.setParent(parent);
 317         container = (parent != null) ? getContainer() : null;
 318         if (oldParent != parent) {
 319             synchronized(this) {
 320                 state |= RELOAD_FLAG;
 321             }
 322         }
 323     }
 324 
 325     /**
 326      * Invoked when the Elements attributes have changed. Recreates the image.
 327      */
 328     public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) {
 329         super.changedUpdate(e,a,f);
 330 
 331         synchronized(this) {
 332             state |= RELOAD_FLAG | RELOAD_IMAGE_FLAG;
 333         }
 334 
 335         // Assume the worst.
 336         preferenceChanged(null, true, true);
 337     }
 338 
 339     /**
 340      * Paints the View.
 341      *
 342      * @param g the rendering surface to use
 343      * @param a the allocated region to render into
 344      * @see View#paint
 345      */
 346     public void paint(Graphics g, Shape a) {
 347         sync();
 348 
 349         Rectangle rect = (a instanceof Rectangle) ? (Rectangle)a :
 350                          a.getBounds();
 351         Rectangle clip = g.getClipBounds();
 352 
 353         fBounds.setBounds(rect);
 354         paintHighlights(g, a);
 355         paintBorder(g, rect);
 356         if (clip != null) {
 357             g.clipRect(rect.x + leftInset, rect.y + topInset,
 358                        rect.width - leftInset - rightInset,
 359                        rect.height - topInset - bottomInset);
 360         }
 361 
 362         Container host = getContainer();
 363         Image img = getImage(host == null || host.isEnabled());
 364         if (img != null) {
 365             if (! hasPixels(img)) {
 366                 // No pixels yet, use the default
 367                 Icon icon = getLoadingImageIcon();
 368                 if (icon != null) {
 369                     icon.paintIcon(host, g,
 370                             rect.x + leftInset, rect.y + topInset);
 371                 }
 372             }
 373             else {
 374                 // Draw the image
 375                 g.drawImage(img, rect.x + leftInset, rect.y + topInset,
 376                             width, height, imageObserver);
 377             }
 378         }
 379         else {
 380             Icon icon = getNoImageIcon();
 381             if (icon != null) {
 382                 icon.paintIcon(host, g,
 383                         rect.x + leftInset, rect.y + topInset);
 384             }
 385             View view = getAltView();
 386             // Paint the view representing the alt text, if its non-null
 387             if (view != null && ((state & WIDTH_FLAG) == 0 ||
 388                                  width > DEFAULT_WIDTH)) {
 389                 // Assume layout along the y direction
 390                 Rectangle altRect = new Rectangle
 391                     (rect.x + leftInset + DEFAULT_WIDTH, rect.y + topInset,
 392                      rect.width - leftInset - rightInset - DEFAULT_WIDTH,
 393                      rect.height - topInset - bottomInset);
 394 
 395                 view.paint(g, altRect);
 396             }
 397         }
 398         if (clip != null) {
 399             // Reset clip.
 400             g.setClip(clip.x, clip.y, clip.width, clip.height);
 401         }
 402     }
 403 
 404     private void paintHighlights(Graphics g, Shape shape) {
 405         if (container instanceof JTextComponent) {
 406             JTextComponent tc = (JTextComponent)container;
 407             Highlighter h = tc.getHighlighter();
 408             if (h instanceof LayeredHighlighter) {
 409                 ((LayeredHighlighter)h).paintLayeredHighlights
 410                     (g, getStartOffset(), getEndOffset(), shape, tc, this);
 411             }
 412         }
 413     }
 414 
 415     private void paintBorder(Graphics g, Rectangle rect) {
 416         Color color = borderColor;
 417 
 418         if ((borderSize > 0 || image == null) && color != null) {
 419             int xOffset = leftInset - borderSize;
 420             int yOffset = topInset - borderSize;
 421             g.setColor(color);
 422             int n = (image == null) ? 1 : borderSize;
 423             for (int counter = 0; counter < n; counter++) {
 424                 g.drawRect(rect.x + xOffset + counter,
 425                            rect.y + yOffset + counter,
 426                            rect.width - counter - counter - xOffset -xOffset-1,
 427                            rect.height - counter - counter -yOffset-yOffset-1);
 428             }
 429         }
 430     }
 431 
 432     /**
 433      * Determines the preferred span for this view along an
 434      * axis.
 435      *
 436      * @param axis may be either X_AXIS or Y_AXIS
 437      * @return   the span the view would like to be rendered into;
 438      *           typically the view is told to render into the span
 439      *           that is returned, although there is no guarantee;
 440      *           the parent may choose to resize or break the view
 441      */
 442     public float getPreferredSpan(int axis) {
 443         sync();
 444 
 445         // If the attributes specified a width/height, always use it!
 446         if (axis == View.X_AXIS && (state & WIDTH_FLAG) == WIDTH_FLAG) {
 447             getPreferredSpanFromAltView(axis);
 448             return width + leftInset + rightInset;
 449         }
 450         if (axis == View.Y_AXIS && (state & HEIGHT_FLAG) == HEIGHT_FLAG) {
 451             getPreferredSpanFromAltView(axis);
 452             return height + topInset + bottomInset;
 453         }
 454 
 455         Image image = getImage();
 456 
 457         if (image != null) {
 458             switch (axis) {
 459             case View.X_AXIS:
 460                 return width + leftInset + rightInset;
 461             case View.Y_AXIS:
 462                 return height + topInset + bottomInset;
 463             default:
 464                 throw new IllegalArgumentException("Invalid axis: " + axis);
 465             }
 466         }
 467         else {
 468             View view = getAltView();
 469             float retValue = 0f;
 470 
 471             if (view != null) {
 472                 retValue = view.getPreferredSpan(axis);
 473             }
 474             switch (axis) {
 475             case View.X_AXIS:
 476                 return retValue + (float)(width + leftInset + rightInset);
 477             case View.Y_AXIS:
 478                 return retValue + (float)(height + topInset + bottomInset);
 479             default:
 480                 throw new IllegalArgumentException("Invalid axis: " + axis);
 481             }
 482         }
 483     }
 484 
 485     /**
 486      * Determines the desired alignment for this view along an
 487      * axis.  This is implemented to give the alignment to the
 488      * bottom of the icon along the y axis, and the default
 489      * along the x axis.
 490      *
 491      * @param axis may be either X_AXIS or Y_AXIS
 492      * @return the desired alignment; this should be a value
 493      *   between 0.0 and 1.0 where 0 indicates alignment at the
 494      *   origin and 1.0 indicates alignment to the full span
 495      *   away from the origin; an alignment of 0.5 would be the
 496      *   center of the view
 497      */
 498     public float getAlignment(int axis) {
 499         switch (axis) {
 500         case View.Y_AXIS:
 501             return vAlign;
 502         default:
 503             return super.getAlignment(axis);
 504         }
 505     }
 506 
 507     /**
 508      * Provides a mapping from the document model coordinate space
 509      * to the coordinate space of the view mapped to it.
 510      *
 511      * @param pos the position to convert
 512      * @param a the allocated region to render into
 513      * @return the bounding box of the given position
 514      * @exception BadLocationException  if the given position does not represent a
 515      *   valid location in the associated document
 516      * @see View#modelToView
 517      */
 518     public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
 519         int p0 = getStartOffset();
 520         int p1 = getEndOffset();
 521         if ((pos >= p0) && (pos <= p1)) {
 522             Rectangle r = a.getBounds();
 523             if (pos == p1) {
 524                 r.x += r.width;
 525             }
 526             r.width = 0;
 527             return r;
 528         }
 529         return null;
 530     }
 531 
 532     /**
 533      * Provides a mapping from the view coordinate space to the logical
 534      * coordinate space of the model.
 535      *
 536      * @param x the X coordinate
 537      * @param y the Y coordinate
 538      * @param a the allocated region to render into
 539      * @return the location within the model that best represents the
 540      *  given point of view
 541      * @see View#viewToModel
 542      */
 543     public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
 544         Rectangle alloc = (Rectangle) a;
 545         if (x < alloc.x + alloc.width) {
 546             bias[0] = Position.Bias.Forward;
 547             return getStartOffset();
 548         }
 549         bias[0] = Position.Bias.Backward;
 550         return getEndOffset();
 551     }
 552 
 553     /**
 554      * Sets the size of the view.  This should cause
 555      * layout of the view if it has any layout duties.
 556      *
 557      * @param width the width &gt;= 0
 558      * @param height the height &gt;= 0
 559      */
 560     public void setSize(float width, float height) {
 561         sync();
 562 
 563         if (getImage() == null) {
 564             View view = getAltView();
 565 
 566             if (view != null) {
 567                 view.setSize(Math.max(0f, width - (float)(DEFAULT_WIDTH + leftInset + rightInset)),
 568                              Math.max(0f, height - (float)(topInset + bottomInset)));
 569             }
 570         }
 571     }
 572 
 573     /**
 574      * Returns true if this image within a link?
 575      */
 576     private boolean isLink() {
 577         return ((state & LINK_FLAG) == LINK_FLAG);
 578     }
 579 
 580     /**
 581      * Returns true if the passed in image has a non-zero width and height.
 582      */
 583     private boolean hasPixels(Image image) {
 584         return image != null &&
 585             (image.getHeight(imageObserver) > 0) &&
 586             (image.getWidth(imageObserver) > 0);
 587     }
 588 
 589     /**
 590      * Returns the preferred span of the View used to display the alt text,
 591      * or 0 if the view does not exist.
 592      */
 593     private float getPreferredSpanFromAltView(int axis) {
 594         if (getImage() == null) {
 595             View view = getAltView();
 596 
 597             if (view != null) {
 598                 return view.getPreferredSpan(axis);
 599             }
 600         }
 601         return 0f;
 602     }
 603 
 604     /**
 605      * Request that this view be repainted.
 606      * Assumes the view is still at its last-drawn location.
 607      */
 608     private void repaint(long delay) {
 609         if (container != null && fBounds != null) {
 610             container.repaint(delay, fBounds.x, fBounds.y, fBounds.width,
 611                                fBounds.height);
 612         }
 613     }
 614 
 615     /**
 616      * Convenience method for getting an integer attribute from the elements
 617      * AttributeSet.
 618      */
 619     private int getIntAttr(HTML.Attribute name, int deflt) {
 620         AttributeSet attr = getElement().getAttributes();
 621         if (attr.isDefined(name)) {             // does not check parents!
 622             int i;
 623             String val = (String)attr.getAttribute(name);
 624             if (val == null) {
 625                 i = deflt;
 626             }
 627             else {
 628                 try{
 629                     i = Math.max(0, Integer.parseInt(val));
 630                 }catch( NumberFormatException x ) {
 631                     i = deflt;
 632                 }
 633             }
 634             return i;
 635         } else
 636             return deflt;
 637     }
 638 
 639     /**
 640      * Makes sure the necessary properties and image is loaded.
 641      */
 642     private void sync() {
 643         int s = state;
 644         if ((s & RELOAD_IMAGE_FLAG) != 0) {
 645             refreshImage();
 646         }
 647         s = state;
 648         if ((s & RELOAD_FLAG) != 0) {
 649             synchronized(this) {
 650                 state = (state | RELOAD_FLAG) ^ RELOAD_FLAG;
 651             }
 652             setPropertiesFromAttributes();
 653         }
 654     }
 655 
 656     /**
 657      * Loads the image and updates the size accordingly. This should be
 658      * invoked instead of invoking <code>loadImage</code> or
 659      * <code>updateImageSize</code> directly.
 660      */
 661     private void refreshImage() {
 662         synchronized(this) {
 663             // clear out width/height/realoadimage flag and set loading flag
 664             state = (state | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG |
 665                      HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG |
 666                                      RELOAD_IMAGE_FLAG);
 667             image = null;
 668             width = height = 0;
 669         }
 670 
 671         try {
 672             // Load the image
 673             loadImage();
 674 
 675             // And update the size params
 676             updateImageSize();
 677         }
 678         finally {
 679             synchronized(this) {
 680                 // Clear out state in case someone threw an exception.
 681                 state = (state | LOADING_FLAG) ^ LOADING_FLAG;
 682             }
 683         }
 684     }
 685 
 686     /**
 687      * Loads the image from the URL <code>getImageURL</code>. This should
 688      * only be invoked from <code>refreshImage</code>.
 689      */
 690     private void loadImage() {
 691         URL src = getImageURL();
 692         Image newImage = null;
 693         if (src != null) {
 694             Dictionary cache = (Dictionary)getDocument().
 695                                     getProperty(IMAGE_CACHE_PROPERTY);
 696             if (cache != null) {
 697                 newImage = (Image)cache.get(src);
 698             }
 699             else {
 700                 newImage = Toolkit.getDefaultToolkit().createImage(src);
 701                 if (newImage != null && getLoadsSynchronously()) {
 702                     // Force the image to be loaded by using an ImageIcon.
 703                     ImageIcon ii = new ImageIcon();
 704                     ii.setImage(newImage);
 705                 }
 706             }
 707         }
 708         image = newImage;
 709     }
 710 
 711     /**
 712      * Recreates and reloads the image.  This should
 713      * only be invoked from <code>refreshImage</code>.
 714      */
 715     private void updateImageSize() {
 716         int newWidth = 0;
 717         int newHeight = 0;
 718         int newState = 0;
 719         Image newImage = getImage();
 720 
 721         if (newImage != null) {
 722             Element elem = getElement();
 723             AttributeSet attr = elem.getAttributes();
 724 
 725             // Get the width/height and set the state ivar before calling
 726             // anything that might cause the image to be loaded, and thus the
 727             // ImageHandler to be called.
 728             newWidth = getIntAttr(HTML.Attribute.WIDTH, -1);
 729             if (newWidth > 0) {
 730                 newState |= WIDTH_FLAG;
 731             }
 732             newHeight = getIntAttr(HTML.Attribute.HEIGHT, -1);
 733             if (newHeight > 0) {
 734                 newState |= HEIGHT_FLAG;
 735             }
 736 
 737             if (newWidth <= 0) {
 738                 newWidth = newImage.getWidth(imageObserver);
 739                 if (newWidth <= 0) {
 740                     newWidth = DEFAULT_WIDTH;
 741                 }
 742             }
 743 
 744             if (newHeight <= 0) {
 745                 newHeight = newImage.getHeight(imageObserver);
 746                 if (newHeight <= 0) {
 747                     newHeight = DEFAULT_HEIGHT;
 748                 }
 749             }
 750 
 751             // Make sure the image starts loading:
 752             if ((newState & (WIDTH_FLAG | HEIGHT_FLAG)) != 0) {
 753                 Toolkit.getDefaultToolkit().prepareImage(newImage, newWidth,
 754                                                          newHeight,
 755                                                          imageObserver);
 756             }
 757             else {
 758                 Toolkit.getDefaultToolkit().prepareImage(newImage, -1, -1,
 759                                                          imageObserver);
 760             }
 761 
 762             boolean createText = false;
 763             synchronized(this) {
 764                 // If imageloading failed, other thread may have called
 765                 // ImageLoader which will null out image, hence we check
 766                 // for it.
 767                 if (image != null) {
 768                     if ((newState & WIDTH_FLAG) == WIDTH_FLAG || width == 0) {
 769                         width = newWidth;
 770                     }
 771                     if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG ||
 772                         height == 0) {
 773                         height = newHeight;
 774                     }
 775                 }
 776                 else {
 777                     createText = true;
 778                     if ((newState & WIDTH_FLAG) == WIDTH_FLAG) {
 779                         width = newWidth;
 780                     }
 781                     if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG) {
 782                         height = newHeight;
 783                     }
 784                 }
 785                 state = state | newState;
 786                 state = (state | LOADING_FLAG) ^ LOADING_FLAG;
 787             }
 788             if (createText) {
 789                 // Only reset if this thread determined image is null
 790                 updateAltTextView();
 791             }
 792         }
 793         else {
 794             width = height = DEFAULT_HEIGHT;
 795             updateAltTextView();
 796         }
 797     }
 798 
 799     /**
 800      * Updates the view representing the alt text.
 801      */
 802     private void updateAltTextView() {
 803         String text = getAltText();
 804 
 805         if (text != null) {
 806             ImageLabelView newView;
 807 
 808             newView = new ImageLabelView(getElement(), text);
 809             synchronized(this) {
 810                 altView = newView;
 811             }
 812         }
 813     }
 814 
 815     /**
 816      * Returns the view to use for alternate text. This may be null.
 817      */
 818     private View getAltView() {
 819         View view;
 820 
 821         synchronized(this) {
 822             view = altView;
 823         }
 824         if (view != null && view.getParent() == null) {
 825             view.setParent(getParent());
 826         }
 827         return view;
 828     }
 829 
 830     /**
 831      * Invokes <code>preferenceChanged</code> on the event displatching
 832      * thread.
 833      */
 834     private void safePreferenceChanged() {
 835         if (SwingUtilities.isEventDispatchThread()) {
 836             Document doc = getDocument();
 837             if (doc instanceof AbstractDocument) {
 838                 ((AbstractDocument)doc).readLock();
 839             }
 840             preferenceChanged(null, true, true);
 841             if (doc instanceof AbstractDocument) {
 842                 ((AbstractDocument)doc).readUnlock();
 843             }
 844         }
 845         else {
 846             SwingUtilities.invokeLater(new Runnable() {
 847                     public void run() {
 848                         safePreferenceChanged();
 849                     }
 850                 });
 851         }
 852     }
 853 
 854     /**
 855      * ImageHandler implements the ImageObserver to correctly update the
 856      * display as new parts of the image become available.
 857      */
 858     private class ImageHandler implements ImageObserver {
 859         // This can come on any thread. If we are in the process of reloading
 860         // the image and determining our state (loading == true) we don't fire
 861         // preference changed, or repaint, we just reset the fWidth/fHeight as
 862         // necessary and return. This is ok as we know when loading finishes
 863         // it will pick up the new height/width, if necessary.
 864         public boolean imageUpdate(Image img, int flags, int x, int y,
 865                                    int newWidth, int newHeight ) {
 866             if (img != image && img != disabledImage ||
 867                 image == null || getParent() == null) {
 868 
 869                 return false;
 870             }
 871 
 872             // Bail out if there was an error:
 873             if ((flags & (ABORT|ERROR)) != 0) {
 874                 repaint(0);
 875                 synchronized(ImageView.this) {
 876                     if (image == img) {
 877                         // Be sure image hasn't changed since we don't
 878                         // initialy synchronize
 879                         image = null;
 880                         if ((state & WIDTH_FLAG) != WIDTH_FLAG) {
 881                             width = DEFAULT_WIDTH;
 882                         }
 883                         if ((state & HEIGHT_FLAG) != HEIGHT_FLAG) {
 884                             height = DEFAULT_HEIGHT;
 885                         }
 886                     } else {
 887                         disabledImage = null;
 888                     }
 889                     if ((state & LOADING_FLAG) == LOADING_FLAG) {
 890                         // No need to resize or repaint, still in the process
 891                         // of loading.
 892                         return false;
 893                     }
 894                 }
 895                 updateAltTextView();
 896                 safePreferenceChanged();
 897                 return false;
 898             }
 899 
 900             if (image == img) {
 901                 // Resize image if necessary:
 902                 short changed = 0;
 903                 if ((flags & ImageObserver.HEIGHT) != 0 && !getElement().
 904                       getAttributes().isDefined(HTML.Attribute.HEIGHT)) {
 905                     changed |= 1;
 906                 }
 907                 if ((flags & ImageObserver.WIDTH) != 0 && !getElement().
 908                       getAttributes().isDefined(HTML.Attribute.WIDTH)) {
 909                     changed |= 2;
 910                 }
 911 
 912                 synchronized(ImageView.this) {
 913                     if ((changed & 1) == 1 && (state & WIDTH_FLAG) == 0) {
 914                         width = newWidth;
 915                     }
 916                     if ((changed & 2) == 2 && (state & HEIGHT_FLAG) == 0) {
 917                         height = newHeight;
 918                     }
 919                     if ((state & LOADING_FLAG) == LOADING_FLAG) {
 920                         // No need to resize or repaint, still in the process of
 921                         // loading.
 922                         return true;
 923                     }
 924                 }
 925                 if (changed != 0) {
 926                     // May need to resize myself, asynchronously:
 927                     safePreferenceChanged();
 928                     return true;
 929                 }
 930             }
 931 
 932             // Repaint when done or when new pixels arrive:
 933             if ((flags & (FRAMEBITS|ALLBITS)) != 0) {
 934                 repaint(0);
 935             }
 936             else if ((flags & SOMEBITS) != 0 && sIsInc) {
 937                 repaint(sIncRate);
 938             }
 939             return ((flags & ALLBITS) == 0);
 940         }
 941     }
 942 
 943 
 944     /**
 945      * ImageLabelView is used if the image can't be loaded, and
 946      * the attribute specified an alt attribute. It overriden a handle of
 947      * methods as the text is hardcoded and does not come from the document.
 948      */
 949     private class ImageLabelView extends InlineView {
 950         private Segment segment;
 951         private Color fg;
 952 
 953         ImageLabelView(Element e, String text) {
 954             super(e);
 955             reset(text);
 956         }
 957 
 958         public void reset(String text) {
 959             segment = new Segment(text.toCharArray(), 0, text.length());
 960         }
 961 
 962         public void paint(Graphics g, Shape a) {
 963             // Don't use supers paint, otherwise selection will be wrong
 964             // as our start/end offsets are fake.
 965             GlyphPainter painter = getGlyphPainter();
 966 
 967             if (painter != null) {
 968                 g.setColor(getForeground());
 969                 painter.paint(this, g, a, getStartOffset(), getEndOffset());
 970             }
 971         }
 972 
 973         public Segment getText(int p0, int p1) {
 974             if (p0 < 0 || p1 > segment.array.length) {
 975                 throw new RuntimeException("ImageLabelView: Stale view");
 976             }
 977             segment.offset = p0;
 978             segment.count = p1 - p0;
 979             return segment;
 980         }
 981 
 982         public int getStartOffset() {
 983             return 0;
 984         }
 985 
 986         public int getEndOffset() {
 987             return segment.array.length;
 988         }
 989 
 990         public View breakView(int axis, int p0, float pos, float len) {
 991             // Don't allow a break
 992             return this;
 993         }
 994 
 995         public Color getForeground() {
 996             View parent;
 997             if (fg == null && (parent = getParent()) != null) {
 998                 Document doc = getDocument();
 999                 AttributeSet attr = parent.getAttributes();
1000 
1001                 if (attr != null && (doc instanceof StyledDocument)) {
1002                     fg = ((StyledDocument)doc).getForeground(attr);
1003                 }
1004             }
1005             return fg;
1006         }
1007     }
1008 }