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