View Javadoc
1   /*
2    * Copyright (c) 2002-2017 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.html;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLIMAGE_BLANK_SRC_AS_EMPTY;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLIMAGE_EMPTY_SRC_DISPLAY_FALSE;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLIMAGE_HTMLELEMENT;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLIMAGE_HTMLUNKNOWNELEMENT;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLIMAGE_INVISIBLE_NOT_AVAILABLE;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLIMAGE_INVISIBLE_NO_SRC;
23  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_IMAGE_COMPLETE_RETURNS_TRUE_FOR_NO_REQUEST;
24  
25  import java.io.File;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.net.URL;
30  import java.util.Iterator;
31  import java.util.Map;
32  
33  import javax.imageio.ImageIO;
34  import javax.imageio.ImageReader;
35  import javax.imageio.stream.ImageInputStream;
36  
37  import org.apache.commons.io.IOUtils;
38  import org.apache.commons.lang3.StringUtils;
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  import org.apache.http.HttpStatus;
42  
43  import com.gargoylesoftware.htmlunit.Page;
44  import com.gargoylesoftware.htmlunit.SgmlPage;
45  import com.gargoylesoftware.htmlunit.WebClient;
46  import com.gargoylesoftware.htmlunit.WebRequest;
47  import com.gargoylesoftware.htmlunit.WebResponse;
48  import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
49  import com.gargoylesoftware.htmlunit.javascript.host.dom.Node;
50  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
51  
52  /**
53   * Wrapper for the HTML element "img".
54   *
55   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
56   * @author David K. Taylor
57   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
58   * @author Ahmed Ashour
59   * @author <a href="mailto:knut.johannes.dahle@gmail.com">Knut Johannes Dahle</a>
60   * @author Ronald Brill
61   * @author Frank Danek
62   * @author Carsten Steul
63   */
64  public class HtmlImage extends HtmlElement {
65  
66      private static final Log LOG = LogFactory.getLog(HtmlImage.class);
67  
68      /** The HTML tag represented by this element. */
69      public static final String TAG_NAME = "img";
70      /** Another HTML tag represented by this element. */
71      public static final String TAG_NAME2 = "image";
72  
73      private final String originalQualifiedName_;
74  
75      private int lastClickX_;
76      private int lastClickY_;
77      private WebResponse imageWebResponse_;
78      private transient ImageData imageData_;
79      private int width_ = -1;
80      private int height_ = -1;
81      private boolean downloaded_;
82      private boolean isComplete_;
83      private boolean onloadInvoked_;
84      private boolean createdByJavascript_;
85  
86      /**
87       * Creates a new instance.
88       *
89       * @param qualifiedName the qualified name of the element type to instantiate
90       * @param page the page that contains this element
91       * @param attributes the initial attributes
92       */
93      HtmlImage(final String qualifiedName, final SgmlPage page, final Map<String, DomAttr> attributes) {
94          super(unifyLocalName(qualifiedName), page, attributes);
95          originalQualifiedName_ = qualifiedName;
96          if (page.getWebClient().getOptions().isDownloadImages()) {
97              try {
98                  downloadImageIfNeeded();
99              }
100             catch (final IOException e) {
101                 if (LOG.isDebugEnabled()) {
102                     LOG.debug("Unable to download image for element " + this);
103                 }
104             }
105         }
106     }
107 
108     private static String unifyLocalName(final String qualifiedName) {
109         if (qualifiedName != null && qualifiedName.endsWith(TAG_NAME2)) {
110             final int pos = qualifiedName.lastIndexOf(TAG_NAME2);
111             return qualifiedName.substring(0, pos) + TAG_NAME;
112         }
113         return qualifiedName;
114     }
115 
116     /**
117      * {@inheritDoc}
118      */
119     @Override
120     protected void onAddedToPage() {
121         doOnLoad();
122         super.onAddedToPage();
123     }
124 
125     /**
126      * {@inheritDoc}
127      */
128     @Override
129     protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String value,
130             final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
131 
132         final HtmlPage htmlPage = getHtmlPageOrNull();
133         if ("src".equals(qualifiedName) && value != ATTRIBUTE_NOT_DEFINED && htmlPage != null) {
134             final String oldValue = getAttributeNS(namespaceURI, qualifiedName);
135             if (!oldValue.equals(value)) {
136                 super.setAttributeNS(namespaceURI, qualifiedName, value, notifyAttributeChangeListeners,
137                         notifyMutationObservers);
138 
139                 // onload handlers may need to be invoked again, and a new image may need to be downloaded
140                 onloadInvoked_ = false;
141                 downloaded_ = false;
142                 isComplete_ = false;
143                 width_ = -1;
144                 height_ = -1;
145                 if (imageData_ != null) {
146                     imageData_.close();
147                     imageData_ = null;
148                 }
149 
150                 final String readyState = htmlPage.getReadyState();
151                 if (READY_STATE_LOADING.equals(readyState)) {
152                     final PostponedAction action = new PostponedAction(getPage()) {
153                         @Override
154                         public void execute() throws Exception {
155                             doOnLoad();
156                         }
157                     };
158                     htmlPage.addAfterLoadAction(action);
159                     return;
160                 }
161                 doOnLoad();
162                 return;
163             }
164         }
165 
166         super.setAttributeNS(namespaceURI, qualifiedName, value, notifyAttributeChangeListeners,
167                 notifyMutationObservers);
168     }
169 
170     /**
171      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
172      *
173      * <p>Executes this element's <tt>onload</tt> handler if it has one. This method also downloads the image
174      * if this element has an <tt>onload</tt> handler (prior to invoking said handler), because applications
175      * sometimes use images to send information to the server and use the <tt>onload</tt> handler to get notified
176      * when the information has been received by the server.</p>
177      *
178      * <p>See <a href="http://www.nabble.com/How-should-we-handle-image.onload--tt9850876.html">here</a> and
179      * <a href="http://www.nabble.com/Image-Onload-Support-td18895781.html">here</a> for the discussion which
180      * lead up to this method.</p>
181      *
182      * <p>This method may be called multiple times, but will only attempt to execute the <tt>onload</tt>
183      * handler the first time it is invoked.</p>
184      */
185     public void doOnLoad() {
186         if (onloadInvoked_) {
187             return;
188         }
189 
190         final HtmlPage htmlPage = getHtmlPageOrNull();
191         if (htmlPage == null) {
192             return; // nothing to do if embedded in XML code
193         }
194 
195         final WebClient client = htmlPage.getWebClient();
196         if (!client.getOptions().isJavaScriptEnabled()) {
197             onloadInvoked_ = true;
198             return;
199         }
200 
201         if (hasEventHandlers("onload") && !getSrcAttribute().isEmpty()) {
202             onloadInvoked_ = true;
203             // An onload handler and source are defined; we need to download the image and then call the onload handler.
204             boolean ok;
205             try {
206                 downloadImageIfNeeded();
207                 final int i = imageWebResponse_.getStatusCode();
208                 ok = (i >= HttpStatus.SC_OK && i < HttpStatus.SC_MULTIPLE_CHOICES) || i == HttpStatus.SC_USE_PROXY;
209             }
210             catch (final IOException e) {
211                 ok = false;
212             }
213             // If the download was a success, trigger the onload handler.
214             if (ok) {
215                 final Event event = new Event(this, Event.TYPE_LOAD);
216                 final Node scriptObject = (Node) getScriptableObject();
217 
218                 final String readyState = htmlPage.getReadyState();
219                 if (READY_STATE_LOADING.equals(readyState)) {
220                     final PostponedAction action = new PostponedAction(getPage()) {
221                         @Override
222                         public void execute() throws Exception {
223                             scriptObject.executeEventLocally(event);
224                         }
225                     };
226                     htmlPage.addAfterLoadAction(action);
227                 }
228                 else {
229                     scriptObject.executeEventLocally(event);
230                 }
231             }
232             else {
233                 if (LOG.isDebugEnabled()) {
234                     LOG.debug("Unable to download image for " + this + "; not firing onload event.");
235                 }
236             }
237         }
238     }
239 
240     /**
241      * Returns the value of the attribute {@code src}. Refer to the
242      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
243      * documentation for details on the use of this attribute.
244      *
245      * @return the value of the attribute {@code src} or an empty string if that attribute isn't defined
246      */
247     public final String getSrcAttribute() {
248         return getSrcAttributeNormalized();
249     }
250 
251     /**
252      * Returns the value of the attribute {@code alt}. Refer to the
253      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
254      * documentation for details on the use of this attribute.
255      *
256      * @return the value of the attribute {@code alt} or an empty string if that attribute isn't defined
257      */
258     public final String getAltAttribute() {
259         return getAttribute("alt");
260     }
261 
262     /**
263      * Returns the value of the attribute {@code name}. Refer to the
264      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
265      * documentation for details on the use of this attribute.
266      *
267      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
268      */
269     public final String getNameAttribute() {
270         return getAttribute("name");
271     }
272 
273     /**
274      * Returns the value of the attribute {@code longdesc}. Refer to the
275      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
276      * documentation for details on the use of this attribute.
277      *
278      * @return the value of the attribute {@code longdesc} or an empty string if that attribute isn't defined
279      */
280     public final String getLongDescAttribute() {
281         return getAttribute("longdesc");
282     }
283 
284     /**
285      * Returns the value of the attribute {@code height}. Refer to the
286      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
287      * documentation for details on the use of this attribute.
288      *
289      * @return the value of the attribute {@code height} or an empty string if that attribute isn't defined
290      */
291     public final String getHeightAttribute() {
292         return getAttribute("height");
293     }
294 
295     /**
296      * Returns the value of the attribute {@code width}. Refer to the
297      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
298      * documentation for details on the use of this attribute.
299      *
300      * @return the value of the attribute {@code width} or an empty string if that attribute isn't defined
301      */
302     public final String getWidthAttribute() {
303         return getAttribute("width");
304     }
305 
306     /**
307      * Returns the value of the attribute {@code usemap}. Refer to the
308      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
309      * documentation for details on the use of this attribute.
310      *
311      * @return the value of the attribute {@code usemap} or an empty string if that attribute isn't defined
312      */
313     public final String getUseMapAttribute() {
314         return getAttribute("usemap");
315     }
316 
317     /**
318      * Returns the value of the attribute {@code ismap}. Refer to the
319      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
320      * documentation for details on the use of this attribute.
321      *
322      * @return the value of the attribute {@code ismap} or an empty string if that attribute isn't defined
323      */
324     public final String getIsmapAttribute() {
325         return getAttribute("ismap");
326     }
327 
328     /**
329      * Returns the value of the attribute {@code align}. Refer to the
330      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
331      * documentation for details on the use of this attribute.
332      *
333      * @return the value of the attribute {@code align} or an empty string if that attribute isn't defined
334      */
335     public final String getAlignAttribute() {
336         return getAttribute("align");
337     }
338 
339     /**
340      * Returns the value of the attribute {@code border}. Refer to the
341      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
342      * documentation for details on the use of this attribute.
343      *
344      * @return the value of the attribute {@code border} or an empty string if that attribute isn't defined
345      */
346     public final String getBorderAttribute() {
347         return getAttribute("border");
348     }
349 
350     /**
351      * Returns the value of the attribute {@code hspace}. Refer to the
352      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
353      * documentation for details on the use of this attribute.
354      *
355      * @return the value of the attribute {@code hspace} or an empty string if that attribute isn't defined
356      */
357     public final String getHspaceAttribute() {
358         return getAttribute("hspace");
359     }
360 
361     /**
362      * Returns the value of the attribute {@code vspace}. Refer to the
363      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
364      * documentation for details on the use of this attribute.
365      *
366      * @return the value of the attribute {@code vspace} or an empty string if that attribute isn't defined
367      */
368     public final String getVspaceAttribute() {
369         return getAttribute("vspace");
370     }
371 
372     /**
373      * <p>Returns the image's actual height (<b>not</b> the image's {@link #getHeightAttribute() height attribute}).</p>
374      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
375      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
376      *
377      * @return the image's actual height
378      * @throws IOException if an error occurs while downloading or reading the image
379      */
380     public int getHeight() throws IOException {
381         if (height_ < 0) {
382             determineWidthAndHeight();
383         }
384         return height_;
385     }
386 
387     /**
388      * <p>Returns the image's actual width (<b>not</b> the image's {@link #getWidthAttribute() width attribute}).</p>
389      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
390      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
391      *
392      * @return the image's actual width
393      * @throws IOException if an error occurs while downloading or reading the image
394      */
395     public int getWidth() throws IOException {
396         if (width_ < 0) {
397             determineWidthAndHeight();
398         }
399         return width_;
400     }
401 
402     /**
403      * <p>Returns the <tt>ImageReader</tt> which can be used to read the image contained by this image element.</p>
404      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
405      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
406      *
407      * @return the <tt>ImageReader</tt> which can be used to read the image contained by this image element
408      * @throws IOException if an error occurs while downloading or reading the image
409      */
410     public ImageReader getImageReader() throws IOException {
411         readImageIfNeeded();
412         return imageData_.getImageReader();
413     }
414 
415     private void determineWidthAndHeight() throws IOException {
416         final ImageReader imgReader = getImageReader();
417         width_ = imgReader.getWidth(0);
418         height_ = imgReader.getHeight(0);
419 
420         // ImageIO creates temp files; to save file handles
421         // we will cache the values and close this directly to free the resources
422         if (imageData_ != null) {
423             imageData_.close();
424             imageData_ = null;
425         }
426     }
427 
428     /**
429      * <p>Returns the <tt>WebResponse</tt> for the image contained by this image element.</p>
430      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
431      * <p>If the image has not already been downloaded and <tt>downloadIfNeeded</tt> is {@code true}, this method
432      * triggers a download and caches the image.</p>
433      *
434      * @param downloadIfNeeded whether or not the image should be downloaded (if it hasn't already been downloaded)
435      * @return {@code null} if no download should be performed and one hasn't already been triggered; otherwise,
436      *         the response received when performing a request for the image referenced by this element
437      * @throws IOException if an error occurs while downloading the image
438      */
439     public WebResponse getWebResponse(final boolean downloadIfNeeded) throws IOException {
440         if (downloadIfNeeded) {
441             downloadImageIfNeeded();
442         }
443         return imageWebResponse_;
444     }
445 
446     /**
447      * <p>Downloads the image contained by this image element.</p>
448      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
449      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
450      *
451      * @throws IOException if an error occurs while downloading the image
452      */
453     private void downloadImageIfNeeded() throws IOException {
454         if (!downloaded_) {
455             // HTMLIMAGE_BLANK_SRC_AS_EMPTY
456             final String src = getSrcAttribute();
457             if (!"".equals(src)
458                     && !(hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY) && StringUtils.isBlank(src))) {
459                 final HtmlPage page = (HtmlPage) getPage();
460                 final WebClient webclient = page.getWebClient();
461 
462                 final URL url = page.getFullyQualifiedUrl(src);
463                 final String accept = webclient.getBrowserVersion().getImgAcceptHeader();
464                 final WebRequest request = new WebRequest(url, accept);
465                 request.setAdditionalHeader("Referer", page.getUrl().toExternalForm());
466                 imageWebResponse_ = webclient.loadWebResponse(request);
467             }
468 
469             if (imageData_ != null) {
470                 imageData_.close();
471                 imageData_ = null;
472             }
473             downloaded_ = true;
474             isComplete_ = hasFeature(JS_IMAGE_COMPLETE_RETURNS_TRUE_FOR_NO_REQUEST)
475                     || (imageWebResponse_ != null && imageWebResponse_.getContentType().contains("image"));
476 
477             width_ = -1;
478             height_ = -1;
479         }
480     }
481 
482     private void readImageIfNeeded() throws IOException {
483         downloadImageIfNeeded();
484         if (imageData_ == null) {
485             if (null == imageWebResponse_) {
486                 throw new IOException("No image response available (src=" + getSrcAttribute() + ")");
487             }
488             @SuppressWarnings("resource")
489             final ImageInputStream iis = ImageIO.createImageInputStream(imageWebResponse_.getContentAsStream());
490             final Iterator<ImageReader> iter = ImageIO.getImageReaders(iis);
491             if (!iter.hasNext()) {
492                 iis.close();
493                 throw new IOException("No image detected in response");
494             }
495             final ImageReader imageReader = iter.next();
496             imageReader.setInput(iis);
497             imageData_ = new ImageData(imageReader);
498 
499             // dispose all others
500             while (iter.hasNext()) {
501                 iter.next().dispose();
502             }
503         }
504     }
505 
506     /**
507      * Simulates clicking this element at the specified position. This only makes sense for
508      * an image map (currently only server side), where the position matters. This method
509      * returns the page contained by this image's window after the click, which may or may not
510      * be the same as the original page, depending on JavaScript event handlers, etc.
511      *
512      * @param x the x position of the click
513      * @param y the y position of the click
514      * @return the page contained by this image's window after the click
515      * @exception IOException if an IO error occurs
516      */
517     public Page click(final int x, final int y) throws IOException {
518         lastClickX_ = x;
519         lastClickY_ = y;
520         return super.click();
521     }
522 
523     /**
524      * Simulates clicking this element at the position <tt>(0, 0)</tt>. This method returns
525      * the page contained by this image's window after the click, which may or may not be the
526      * same as the original page, depending on JavaScript event handlers, etc.
527      *
528      * @return the page contained by this image's window after the click
529      * @exception IOException if an IO error occurs
530      */
531     @Override
532     @SuppressWarnings("unchecked")
533     public Page click() throws IOException {
534         return click(0, 0);
535     }
536 
537     /**
538      * Performs the click action on the enclosing A tag (if any).
539      * {@inheritDoc}
540      * @throws IOException if an IO error occurred
541      */
542     @Override
543     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
544         if (getUseMapAttribute() != ATTRIBUTE_NOT_DEFINED) {
545             // remove initial '#'
546             final String mapName = getUseMapAttribute().substring(1);
547             final HtmlElement doc = ((HtmlPage) getPage()).getDocumentElement();
548             final HtmlMap map = doc.getOneHtmlElementByAttribute("map", "name", mapName);
549             for (final DomElement element : map.getChildElements()) {
550                 if (element instanceof HtmlArea) {
551                     final HtmlArea area = (HtmlArea) element;
552                     if (area.containsPoint(lastClickX_, lastClickY_)) {
553                         area.doClickStateUpdate(shiftKey, ctrlKey);
554                         return false;
555                     }
556                 }
557             }
558         }
559         final HtmlAnchor anchor = (HtmlAnchor) getEnclosingElement("a");
560         if (anchor == null) {
561             return false;
562         }
563         if (getIsmapAttribute() != ATTRIBUTE_NOT_DEFINED) {
564             final String suffix = "?" + lastClickX_ + "," + lastClickY_;
565             anchor.doClickStateUpdate(false, false, suffix);
566             return false;
567         }
568         anchor.doClickStateUpdate(shiftKey, ctrlKey);
569         return false;
570     }
571 
572     /**
573      * Saves this image as the specified file.
574      * @param file the file to save to
575      * @throws IOException if an IO error occurs
576      */
577     public void saveAs(final File file) throws IOException {
578         downloadImageIfNeeded();
579         if (null != imageWebResponse_) {
580             try (FileOutputStream fileOut = new FileOutputStream(file);
581                     InputStream inputStream = imageWebResponse_.getContentAsStream()) {
582                 IOUtils.copy(inputStream, fileOut);
583             }
584         }
585     }
586 
587     /**
588      * {@inheritDoc}
589      */
590     @Override
591     public DisplayStyle getDefaultStyleDisplay() {
592         return DisplayStyle.INLINE;
593     }
594 
595     /**
596      * Wraps the ImageReader for an HtmlImage. This is necessary because an object with a finalize()
597      * method is only garbage collected after the method has been run. Which causes all referenced
598      * objects to also not be garbage collected until this happens. Because a HtmlImage references a lot
599      * of objects which could all be garbage collected without impacting the ImageReader it is better to
600      * wrap it in another class.
601      */
602     static final class ImageData implements AutoCloseable {
603 
604         private final ImageReader imageReader_;
605 
606         ImageData(final ImageReader imageReader) {
607             imageReader_ = imageReader;
608         }
609 
610         public ImageReader getImageReader() {
611             return imageReader_;
612         }
613 
614         /**
615          * {@inheritDoc}
616          */
617         @Override
618         protected void finalize() throws Throwable {
619             close();
620             super.finalize();
621         }
622 
623         @Override
624         public void close() {
625             if (imageReader_ != null) {
626                 try {
627                     try (ImageInputStream stream = (ImageInputStream) imageReader_.getInput()) {
628                         // nothing
629                     }
630                 }
631                 catch (final IOException e) {
632                     LOG.error(e.getMessage(), e);
633                 }
634                 finally {
635                     imageReader_.setInput(null);
636                     imageReader_.dispose();
637                 }
638             }
639         }
640     }
641 
642     /**
643      * @return true if the image was successfully downloaded
644      */
645     public boolean isComplete() {
646         return isComplete_ || (hasFeature(JS_IMAGE_COMPLETE_RETURNS_TRUE_FOR_NO_REQUEST)
647                                 ? ATTRIBUTE_NOT_DEFINED == getSrcAttribute()
648                                 : imageData_ != null);
649     }
650 
651     /**
652      * @return true if the image was successfully downloaded
653      * @deprecated as of 2.26, please use {@link #isComplete()} instead
654      */
655     @Deprecated
656     public boolean getComplete() {
657         return isComplete();
658     }
659 
660     /**
661      * {@inheritDoc}
662      */
663     @Override
664     public boolean isDisplayed() {
665         final String src = getSrcAttribute();
666         if (hasFeature(HTMLIMAGE_INVISIBLE_NO_SRC)
667                 && (ATTRIBUTE_NOT_DEFINED == src
668                     || (hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY) && StringUtils.isBlank(src))
669                     || (hasFeature(HTMLIMAGE_EMPTY_SRC_DISPLAY_FALSE) && StringUtils.isEmpty(src)))) {
670             return false;
671         }
672 
673         if (hasFeature(HTMLIMAGE_INVISIBLE_NOT_AVAILABLE)) {
674             try {
675                 downloadImageIfNeeded();
676                 if (imageWebResponse_ == null || !imageWebResponse_.getContentType().contains("image")) {
677                     return false;
678                 }
679             }
680             catch (final IOException e) {
681                 return false;
682             }
683         }
684         return super.isDisplayed();
685     }
686 
687     /**
688      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
689      *
690      * Marks this frame as created by javascript. This is needed to handle
691      * some special IE behavior.
692      */
693     public void markAsCreatedByJavascript() {
694         createdByJavascript_ = true;
695     }
696 
697     /**
698      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
699      *
700      * Returns true if this frame was created by javascript. This is needed to handle
701      * some special IE behavior.
702      * @return true or false
703      */
704     public boolean wasCreatedByJavascript() {
705         return createdByJavascript_;
706     }
707 
708     /**
709      * Returns the original element qualified name,
710      * this is needed to differentiate between <tt>img</tt> and <tt>image</tt>.
711      * @return the original element qualified name
712      */
713     public String getOriginalQualifiedName() {
714         return originalQualifiedName_;
715     }
716 
717     /**
718      * {@inheritDoc}
719      */
720     @Override
721     public String getLocalName() {
722         if (wasCreatedByJavascript()
723                 && (hasFeature(HTMLIMAGE_HTMLELEMENT) || hasFeature(HTMLIMAGE_HTMLUNKNOWNELEMENT))) {
724             return originalQualifiedName_;
725         }
726         return super.getLocalName();
727     }
728 }