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