View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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    * https://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 org.htmlunit.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.HTMLIMAGE_BLANK_SRC_AS_EMPTY;
18  import static org.htmlunit.BrowserVersionFeatures.HTMLIMAGE_EMPTY_SRC_DISPLAY_FALSE;
19  import static org.htmlunit.BrowserVersionFeatures.HTMLIMAGE_HTMLELEMENT;
20  import static org.htmlunit.BrowserVersionFeatures.HTMLIMAGE_HTMLUNKNOWNELEMENT;
21  import static org.htmlunit.BrowserVersionFeatures.JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0;
22  import static org.htmlunit.BrowserVersionFeatures.JS_IMAGE_WIDTH_HEIGHT_RETURNS_24x24_0x0;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.net.MalformedURLException;
29  import java.net.URL;
30  import java.nio.file.Files;
31  import java.util.Map;
32  
33  import org.apache.commons.io.IOUtils;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.htmlunit.BrowserVersion;
37  import org.htmlunit.Page;
38  import org.htmlunit.ScriptResult;
39  import org.htmlunit.SgmlPage;
40  import org.htmlunit.WebClient;
41  import org.htmlunit.WebRequest;
42  import org.htmlunit.WebResponse;
43  import org.htmlunit.http.HttpStatus;
44  import org.htmlunit.javascript.AbstractJavaScriptEngine;
45  import org.htmlunit.javascript.PostponedAction;
46  import org.htmlunit.javascript.host.dom.Document;
47  import org.htmlunit.javascript.host.event.Event;
48  import org.htmlunit.javascript.host.event.MouseEvent;
49  import org.htmlunit.platform.Platform;
50  import org.htmlunit.platform.geom.IntDimension2D;
51  import org.htmlunit.platform.image.ImageData;
52  import org.htmlunit.util.StringUtils;
53  import org.htmlunit.util.UrlUtils;
54  
55  /**
56   * Wrapper for the HTML element "img".
57   *
58   * @author Mike Bowler
59   * @author David K. Taylor
60   * @author Christian Sell
61   * @author Ahmed Ashour
62   * @author Knut Johannes Dahle
63   * @author Ronald Brill
64   * @author Frank Danek
65   * @author Carsten Steul
66   * @author Alex Gorbatovsky
67   */
68  public class HtmlImage extends HtmlElement {
69  
70      private static final Log LOG = LogFactory.getLog(HtmlImage.class);
71  
72      /** The HTML tag represented by this element. */
73      public static final String TAG_NAME = "img";
74      /** Another HTML tag represented by this element. */
75      public static final String TAG_NAME2 = "image";
76  
77      private final String originalQualifiedName_;
78  
79      private int lastClickX_ = -1;
80      private int lastClickY_ = -1;
81      private WebResponse imageWebResponse_;
82      private transient ImageData imageData_;
83      private int width_ = -1;
84      private int height_ = -1;
85      private boolean downloaded_;
86      private boolean isComplete_;
87      private boolean onloadProcessed_;
88      private boolean createdByJavascript_;
89  
90      /**
91       * Creates a new instance.
92       *
93       * @param qualifiedName the qualified name of the element type to instantiate
94       * @param page the page that contains this element
95       * @param attributes the initial attributes
96       */
97      HtmlImage(final String qualifiedName, final SgmlPage page, final Map<String, DomAttr> attributes) {
98          super(unifyLocalName(qualifiedName), page, attributes);
99          originalQualifiedName_ = qualifiedName;
100         if (page.getWebClient().getOptions().isDownloadImages()) {
101             try {
102                 downloadImageIfNeeded();
103             }
104             catch (final IOException e) {
105                 if (LOG.isDebugEnabled()) {
106                     LOG.debug("Unable to download image for element " + this);
107                 }
108             }
109         }
110     }
111 
112     private static String unifyLocalName(final String qualifiedName) {
113         if (qualifiedName != null && qualifiedName.endsWith(TAG_NAME2)) {
114             final int pos = qualifiedName.lastIndexOf(TAG_NAME2);
115             return qualifiedName.substring(0, pos) + TAG_NAME;
116         }
117         return qualifiedName;
118     }
119 
120     /**
121      * {@inheritDoc}
122      */
123     @Override
124     protected void onAddedToPage() {
125         doOnLoad();
126         super.onAddedToPage();
127     }
128 
129     /**
130      * {@inheritDoc}
131      */
132     @Override
133     protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String value,
134             final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
135 
136         final HtmlPage htmlPage = getHtmlPageOrNull();
137         final String qualifiedNameLC = StringUtils.toRootLowerCase(qualifiedName);
138         if (SRC_ATTRIBUTE.equals(qualifiedNameLC) && value != ATTRIBUTE_NOT_DEFINED && htmlPage != null) {
139             final String oldValue = getAttributeNS(namespaceURI, qualifiedNameLC);
140             if (!oldValue.equals(value)) {
141                 super.setAttributeNS(namespaceURI, qualifiedNameLC, value, notifyAttributeChangeListeners,
142                         notifyMutationObservers);
143 
144                 // onload handlers may need to be invoked again, and a new image may need to be downloaded
145                 onloadProcessed_ = false;
146                 downloaded_ = false;
147                 isComplete_ = false;
148                 width_ = -1;
149                 height_ = -1;
150                 try {
151                     closeImageData();
152                 }
153                 catch (final Exception e) {
154                     LOG.error(e.getMessage(), e);
155                 }
156 
157                 final String readyState = htmlPage.getReadyState();
158                 if (READY_STATE_LOADING.equals(readyState)) {
159                     final PostponedAction action = new PostponedAction(getPage(), "HtmlImage.setAttributeNS") {
160                         @Override
161                         public void execute() {
162                             doOnLoad();
163                         }
164                     };
165                     htmlPage.addAfterLoadAction(action);
166                     return;
167                 }
168                 doOnLoad();
169                 return;
170             }
171         }
172 
173         super.setAttributeNS(namespaceURI, qualifiedNameLC, value, notifyAttributeChangeListeners,
174                 notifyMutationObservers);
175     }
176 
177     /**
178      * {@inheritDoc}
179      */
180     @Override
181     public void processImportNode(final Document doc) {
182         URL oldUrl = null;
183         final String src = getSrcAttribute();
184         HtmlPage htmlPage = getHtmlPageOrNull();
185         try {
186             if (htmlPage != null) {
187                 oldUrl = htmlPage.getFullyQualifiedUrl(src);
188             }
189         }
190         catch (final MalformedURLException ignored) {
191             // ignore
192         }
193 
194         super.processImportNode(doc);
195 
196         URL url = null;
197         htmlPage = getHtmlPageOrNull();
198         try {
199             if (htmlPage != null) {
200                 url = htmlPage.getFullyQualifiedUrl(src);
201             }
202         }
203         catch (final MalformedURLException ignored) {
204             // ignore
205         }
206 
207         if (oldUrl == null || !UrlUtils.sameFile(oldUrl, url)) {
208             // image has to be reloaded
209             lastClickX_ = -1;
210             lastClickY_ = -1;
211             imageWebResponse_ = null;
212             imageData_ = null;
213             width_ = -1;
214             height_ = -1;
215             downloaded_ = false;
216             isComplete_ = false;
217             onloadProcessed_ = false;
218             createdByJavascript_ = true;
219         }
220 
221         if (htmlPage == null) {
222             return; // nothing to do if embedded in XML code
223         }
224 
225         if (htmlPage.getWebClient().getOptions().isDownloadImages()) {
226             try {
227                 downloadImageIfNeeded();
228             }
229             catch (final IOException e) {
230                 if (LOG.isDebugEnabled()) {
231                     LOG.debug("Unable to download image for element " + this);
232                 }
233             }
234         }
235     }
236 
237     /**
238      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
239      *
240      * <p>Executes this element's <code>onload</code> or <code>onerror</code> handler. This method downloads the image
241      * if either of these handlers are present (prior to invoking the resulting handler), because applications
242      * sometimes use images to send information to the server and use these handlers to get notified when the
243      * information has been received by the server.</p>
244      *
245      * <p>See <a href="http://www.nabble.com/How-should-we-handle-image.onload--tt9850876.html">here</a> and
246      * <a href="http://www.nabble.com/Image-Onload-Support-td18895781.html">here</a> for the discussion which
247      * lead up to this method.</p>
248      *
249      * <p>This method may be called multiple times, but will only attempt to execute the <code>onload</code> or
250      * <code>onerror</code> handler the first time it is invoked.</p>
251      */
252     public void doOnLoad() {
253         if (onloadProcessed_) {
254             return;
255         }
256 
257         final HtmlPage htmlPage = getHtmlPageOrNull();
258         if (htmlPage == null) {
259             return; // nothing to do if embedded in XML code
260         }
261 
262         final WebClient client = htmlPage.getWebClient();
263 
264         final boolean hasEventHandler = hasEventHandlers("onload") || hasEventHandlers("onerror");
265         if (((hasEventHandler && client.isJavaScriptEnabled())
266                 || client.getOptions().isDownloadImages()) && hasAttribute(SRC_ATTRIBUTE)) {
267             boolean loadSuccessful = false;
268             final boolean tryDownload;
269             if (hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY)) {
270                 tryDownload = !StringUtils.isBlank(getSrcAttribute());
271             }
272             else {
273                 tryDownload = !getSrcAttribute().isEmpty();
274             }
275             if (tryDownload) {
276                 // We need to download the image and then call the resulting handler.
277                 try {
278                     downloadImageIfNeeded();
279                     // if the download was a success
280                     if (imageWebResponse_.isSuccess()) {
281                         if (imageWebResponse_.getStatusCode() != HttpStatus.NO_CONTENT_204) {
282                             loadSuccessful = true; // Trigger the onload handler
283                         }
284                     }
285                 }
286                 catch (final IOException e) {
287                     if (LOG.isDebugEnabled()) {
288                         LOG.debug("IOException while downloading image for '" + this + "'", e);
289                     }
290                 }
291             }
292 
293             if (!client.isJavaScriptEnabled()) {
294                 onloadProcessed_ = true;
295                 return;
296             }
297 
298             if (!hasEventHandler) {
299                 return;
300             }
301 
302             onloadProcessed_ = true;
303             final Event event = new Event(this, loadSuccessful ? Event.TYPE_LOAD : Event.TYPE_ERROR);
304             if (LOG.isDebugEnabled()) {
305                 LOG.debug("Firing the " + event.getType() + " event for '" + this + "'.");
306             }
307 
308             if (READY_STATE_LOADING.equals(htmlPage.getReadyState())) {
309                 final PostponedAction action = new PostponedAction(getPage(), "HtmlImage.doOnLoad") {
310                     @Override
311                     public void execute() {
312                         HtmlImage.this.fireEvent(event);
313                     }
314                 };
315                 htmlPage.addAfterLoadAction(action);
316             }
317             else {
318                 final AbstractJavaScriptEngine<?> jsEngine = client.getJavaScriptEngine();
319                 if (jsEngine.isScriptRunning()) {
320                     final PostponedAction action = new PostponedAction(getPage(), "HtmlImage.doOnLoad") {
321                         @Override
322                         public void execute() {
323                             HtmlImage.this.fireEvent(event);
324                         }
325                     };
326                     jsEngine.addPostponedAction(action);
327                 }
328                 else {
329                     fireEvent(event);
330                 }
331             }
332         }
333     }
334 
335     /**
336      * Returns the value of the attribute {@code src}. Refer to the
337      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
338      * documentation for details on the use of this attribute.
339      *
340      * @return the value of the attribute {@code src} or an empty string if that attribute isn't defined
341      */
342     public final String getSrcAttribute() {
343         return getSrcAttributeNormalized();
344     }
345 
346     /**
347      * Returns the value of the {@code src} value.
348      * @return the value of the {@code src} value
349      */
350     public String getSrc() {
351         final String src = getSrcAttribute();
352         if (StringUtils.isEmptyString(src)) {
353             return src;
354         }
355         try {
356             final HtmlPage page = (HtmlPage) getPage();
357             return page.getFullyQualifiedUrl(src).toExternalForm();
358         }
359         catch (final MalformedURLException e) {
360             final String msg = "Unable to create fully qualified URL for src attribute of image " + e.getMessage();
361             throw new RuntimeException(msg, e);
362         }
363     }
364 
365     /**
366      * Returns the value of the attribute {@code alt}. Refer to the
367      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
368      * documentation for details on the use of this attribute.
369      *
370      * @return the value of the attribute {@code alt} or an empty string if that attribute isn't defined
371      */
372     public final String getAltAttribute() {
373         return getAttributeDirect("alt");
374     }
375 
376     /**
377      * Returns the value of the attribute {@code name}. Refer to the
378      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
379      * documentation for details on the use of this attribute.
380      *
381      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
382      */
383     public final String getNameAttribute() {
384         return getAttributeDirect(NAME_ATTRIBUTE);
385     }
386 
387     /**
388      * Returns the value of the attribute {@code longdesc}. Refer to the
389      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
390      * documentation for details on the use of this attribute.
391      *
392      * @return the value of the attribute {@code longdesc} or an empty string if that attribute isn't defined
393      */
394     public final String getLongDescAttribute() {
395         return getAttributeDirect("longdesc");
396     }
397 
398     /**
399      * Returns the value of the attribute {@code height}. Refer to the
400      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
401      * documentation for details on the use of this attribute.
402      *
403      * @return the value of the attribute {@code height} or an empty string if that attribute isn't defined
404      */
405     public final String getHeightAttribute() {
406         return getAttributeDirect("height");
407     }
408 
409     /**
410      * Returns the value of the attribute {@code width}. Refer to the
411      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
412      * documentation for details on the use of this attribute.
413      *
414      * @return the value of the attribute {@code width} or an empty string if that attribute isn't defined
415      */
416     public final String getWidthAttribute() {
417         return getAttributeDirect("width");
418     }
419 
420     /**
421      * Returns the value of the attribute {@code usemap}. Refer to the
422      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
423      * documentation for details on the use of this attribute.
424      *
425      * @return the value of the attribute {@code usemap} or an empty string if that attribute isn't defined
426      */
427     public final String getUseMapAttribute() {
428         return getAttributeDirect("usemap");
429     }
430 
431     /**
432      * Returns the value of the attribute {@code ismap}. Refer to the
433      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
434      * documentation for details on the use of this attribute.
435      *
436      * @return the value of the attribute {@code ismap} or an empty string if that attribute isn't defined
437      */
438     public final String getIsmapAttribute() {
439         return getAttributeDirect("ismap");
440     }
441 
442     /**
443      * Returns the value of the attribute {@code align}. Refer to the
444      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
445      * documentation for details on the use of this attribute.
446      *
447      * @return the value of the attribute {@code align} or an empty string if that attribute isn't defined
448      */
449     public final String getAlignAttribute() {
450         return getAttributeDirect("align");
451     }
452 
453     /**
454      * Returns the value of the attribute {@code border}. Refer to the
455      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
456      * documentation for details on the use of this attribute.
457      *
458      * @return the value of the attribute {@code border} or an empty string if that attribute isn't defined
459      */
460     public final String getBorderAttribute() {
461         return getAttributeDirect("border");
462     }
463 
464     /**
465      * Returns the value of the attribute {@code hspace}. Refer to the
466      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
467      * documentation for details on the use of this attribute.
468      *
469      * @return the value of the attribute {@code hspace} or an empty string if that attribute isn't defined
470      */
471     public final String getHspaceAttribute() {
472         return getAttributeDirect("hspace");
473     }
474 
475     /**
476      * Returns the value of the attribute {@code vspace}. Refer to the
477      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
478      * documentation for details on the use of this attribute.
479      *
480      * @return the value of the attribute {@code vspace} or an empty string if that attribute isn't defined
481      */
482     public final String getVspaceAttribute() {
483         return getAttributeDirect("vspace");
484     }
485 
486     /**
487      * <p>Returns the image's actual height (<b>not</b> the image's {@link #getHeightAttribute() height attribute}).</p>
488      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
489      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
490      *
491      * @return the image's actual height
492      * @throws IOException if an error occurs while downloading or reading the image
493      */
494     public int getHeight() throws IOException {
495         if (height_ < 0) {
496             determineWidthAndHeight();
497         }
498         return height_;
499     }
500 
501     /**
502      * Returns the value same value as the js height property.
503      * @return the value of the {@code height} property
504      */
505     public int getHeightOrDefault() {
506         final String height = getHeightAttribute();
507 
508         if (ATTRIBUTE_NOT_DEFINED != height) {
509             try {
510                 return Integer.parseInt(height);
511             }
512             catch (final NumberFormatException ignored) {
513                 // ignore
514             }
515         }
516 
517         final String src = getSrcAttribute();
518         if (ATTRIBUTE_NOT_DEFINED == src) {
519             final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion();
520             if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)
521                     || browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_24x24_0x0)) {
522                 return 0;
523             }
524             return 24;
525         }
526 
527         final WebClient webClient = getPage().getWebClient();
528         final BrowserVersion browserVersion = webClient.getBrowserVersion();
529         if (StringUtils.isEmptyOrNull(src)) {
530             return 0;
531         }
532         if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)
533                 && StringUtils.isBlank(src)) {
534             return 0;
535         }
536 
537         try {
538             return getHeight();
539         }
540         catch (final IOException e) {
541             if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)) {
542                 return 16;
543             }
544             return 24;
545         }
546     }
547 
548     /**
549      * <p>Returns the image's actual width (<b>not</b> the image's {@link #getWidthAttribute() width attribute}).</p>
550      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
551      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
552      *
553      * @return the image's actual width
554      * @throws IOException if an error occurs while downloading or reading the image
555      */
556     public int getWidth() throws IOException {
557         if (width_ < 0) {
558             determineWidthAndHeight();
559         }
560         return width_;
561     }
562 
563     /**
564      * Returns the value same value as the js width property.
565      * @return the value of the {@code width} property
566      */
567     public int getWidthOrDefault() {
568         final String widthAttrib = getWidthAttribute();
569 
570         if (ATTRIBUTE_NOT_DEFINED != widthAttrib) {
571             try {
572                 return Integer.parseInt(widthAttrib);
573             }
574             catch (final NumberFormatException ignored) {
575                 // ignore
576             }
577         }
578 
579         final String src = getSrcAttribute();
580         if (ATTRIBUTE_NOT_DEFINED == src) {
581             final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion();
582             if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)
583                     || browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_24x24_0x0)) {
584                 return 0;
585             }
586             return 24;
587         }
588 
589         final WebClient webClient = getPage().getWebClient();
590         final BrowserVersion browserVersion = webClient.getBrowserVersion();
591         if (StringUtils.isEmptyOrNull(src)) {
592             return 0;
593         }
594         if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)
595                 && StringUtils.isBlank(src)) {
596             return 0;
597         }
598 
599         try {
600             return getWidth();
601         }
602         catch (final IOException e) {
603             if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)) {
604                 return 16;
605             }
606             return 24;
607         }
608     }
609 
610     /**
611      * @return the {@link ImageData} of this image
612      * @throws IOException in case of error
613      */
614     public ImageData getImageData() throws IOException {
615         readImageIfNeeded();
616         return imageData_;
617     }
618 
619     private void determineWidthAndHeight() throws IOException {
620         readImageIfNeeded();
621 
622         final IntDimension2D dim = imageData_.getWidthHeight();
623         width_ = dim.getWidth();
624         height_ = dim.getHeight();
625 
626         // ImageIO creates temp files; to save file handles
627         // we will cache the values and close this directly to free the resources
628         closeImageData();
629     }
630 
631     private void closeImageData() throws IOException {
632         if (imageData_ != null) {
633             try {
634                 imageData_.close();
635             }
636             catch (final IOException e) {
637                 throw e;
638             }
639             catch (final Exception ex) {
640                 throw new IOException("Exception during close()", ex);
641             }
642             imageData_ = null;
643         }
644     }
645 
646     /**
647      * <p>Returns the <code>WebResponse</code> for the image contained by this image element.</p>
648      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
649      * <p>If the image has not already been downloaded and <code>downloadIfNeeded</code> is {@code true}, this method
650      * triggers a download and caches the image.</p>
651      *
652      * @param downloadIfNeeded whether or not the image should be downloaded (if it hasn't already been downloaded)
653      * @return {@code null} if no download should be performed and one hasn't already been triggered; otherwise,
654      *         the response received when performing a request for the image referenced by this element
655      * @throws IOException if an error occurs while downloading the image
656      */
657     public WebResponse getWebResponse(final boolean downloadIfNeeded) throws IOException {
658         if (downloadIfNeeded) {
659             downloadImageIfNeeded();
660         }
661         return imageWebResponse_;
662     }
663 
664     /**
665      * <p>Downloads the image contained by this image element.</p>
666      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
667      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
668      *
669      * @throws IOException if an error occurs while downloading the image
670      */
671     private void downloadImageIfNeeded() throws IOException {
672         if (!downloaded_) {
673             // HTMLIMAGE_BLANK_SRC_AS_EMPTY
674             final String src = getSrcAttribute();
675 
676             if (!StringUtils.isEmptyString(src)) {
677                 final HtmlPage page = (HtmlPage) getPage();
678                 final WebClient webClient = page.getWebClient();
679                 final BrowserVersion browser = webClient.getBrowserVersion();
680 
681                 if (!(browser.hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY)
682                         && StringUtils.isBlank(src))) {
683                     final URL url = page.getFullyQualifiedUrl(src);
684                     final WebRequest request = new WebRequest(url, browser.getImgAcceptHeader(),
685                                                                     browser.getAcceptEncodingHeader());
686                     request.setCharset(page.getCharset());
687                     request.setRefererHeader(page.getUrl());
688                     imageWebResponse_ = webClient.loadWebResponse(request);
689                 }
690             }
691 
692             closeImageData();
693 
694             downloaded_ = true;
695             isComplete_ = true;
696 
697             width_ = -1;
698             height_ = -1;
699         }
700     }
701 
702     private void readImageIfNeeded() throws IOException {
703         downloadImageIfNeeded();
704         if (imageData_ == null) {
705             if (null == imageWebResponse_) {
706                 throw new IOException("No image response available (src='" + getSrcAttribute() + "')");
707             }
708             imageData_ = Platform.buildImageData(imageWebResponse_.getContentAsStream());
709         }
710     }
711 
712     /**
713      * Simulates clicking this element at the specified position. This only makes sense for
714      * an image map (currently only server side), where the position matters. This method
715      * returns the page contained by this image's window after the click, which may or may not
716      * be the same as the original page, depending on JavaScript event handlers, etc.
717      *
718      * @param x the x position of the click
719      * @param y the y position of the click
720      * @return the page contained by this image's window after the click
721      * @exception IOException if an IO error occurs
722      */
723     public Page click(final int x, final int y) throws IOException {
724         lastClickX_ = x;
725         lastClickY_ = y;
726         try {
727             return super.click();
728         }
729         finally {
730             lastClickX_ = -1;
731             lastClickY_ = -1;
732         }
733     }
734 
735     /**
736      * Simulates clicking this element at the position <code>(0, 0)</code>. This method returns
737      * the page contained by this image's window after the click, which may or may not be the
738      * same as the original page, depending on JavaScript event handlers, etc.
739      *
740      * @return the page contained by this image's window after the click
741      * @exception IOException if an IO error occurs
742      */
743     @Override
744     @SuppressWarnings("unchecked")
745     public Page click() throws IOException {
746         return click(0, 0);
747     }
748 
749     /**
750      * Performs the click action on the enclosing A tag (if any).
751      * {@inheritDoc}
752      * @throws IOException if an IO error occurred
753      */
754     @Override
755     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
756         if (ATTRIBUTE_NOT_DEFINED != getUseMapAttribute()) {
757             // remove initial '#'
758             final String mapName = getUseMapAttribute().substring(1);
759             final HtmlElement doc = ((HtmlPage) getPage()).getDocumentElement();
760             final HtmlMap map = doc.getOneHtmlElementByAttribute("map", NAME_ATTRIBUTE, mapName);
761             for (final DomElement element : map.getChildElements()) {
762                 if (element instanceof HtmlArea) {
763                     final HtmlArea area = (HtmlArea) element;
764                     if (area.containsPoint(Math.max(lastClickX_, 0), Math.max(lastClickY_, 0))) {
765                         area.doClickStateUpdate(shiftKey, ctrlKey);
766                         return false;
767                     }
768                 }
769             }
770         }
771         final HtmlAnchor anchor = (HtmlAnchor) getEnclosingElement("a");
772         if (anchor == null) {
773             return false;
774         }
775         if (ATTRIBUTE_NOT_DEFINED != getIsmapAttribute()) {
776             final String suffix = "?" + Math.max(lastClickX_, 0) + "," + Math.max(lastClickY_, 0);
777             anchor.doClickStateUpdate(false, false, suffix);
778             return false;
779         }
780         anchor.doClickStateUpdate(shiftKey, ctrlKey);
781         return false;
782     }
783 
784     /**
785      * Saves this image as the specified file.
786      * @param file the file to save to
787      * @throws IOException if an IO error occurs
788      */
789     public void saveAs(final File file) throws IOException {
790         downloadImageIfNeeded();
791         if (null != imageWebResponse_) {
792             try (OutputStream fos = Files.newOutputStream(file.toPath());
793                     InputStream inputStream = imageWebResponse_.getContentAsStream()) {
794                 IOUtils.copy(inputStream, fos);
795             }
796         }
797     }
798 
799     /**
800      * {@inheritDoc}
801      */
802     @Override
803     public DisplayStyle getDefaultStyleDisplay() {
804         return DisplayStyle.INLINE;
805     }
806 
807     /**
808      * @return true if the image was successfully downloaded
809      */
810     public boolean isComplete() {
811         return isComplete_ || ATTRIBUTE_NOT_DEFINED == getSrcAttribute();
812     }
813 
814     /**
815      * {@inheritDoc}
816      */
817     @Override
818     public boolean isDisplayed() {
819         final String src = getSrcAttribute();
820         if (ATTRIBUTE_NOT_DEFINED == src) {
821             return false;
822         }
823         if (hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY) && StringUtils.isBlank(src)) {
824             return false;
825         }
826         if (hasFeature(HTMLIMAGE_EMPTY_SRC_DISPLAY_FALSE) && StringUtils.isEmptyOrNull(src)) {
827             return false;
828         }
829 
830         return super.isDisplayed();
831     }
832 
833     /**
834      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
835      *
836      * Marks this frame as created by javascript.
837      */
838     public void markAsCreatedByJavascript() {
839         createdByJavascript_ = true;
840     }
841 
842     /**
843      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
844      *
845      * Returns true if this frame was created by javascript.
846      * @return true or false
847      */
848     public boolean wasCreatedByJavascript() {
849         return createdByJavascript_;
850     }
851 
852     /**
853      * Returns the original element qualified name,
854      * this is needed to differentiate between <code>img</code> and <code>image</code>.
855      * @return the original element qualified name
856      */
857     public String getOriginalQualifiedName() {
858         return originalQualifiedName_;
859     }
860 
861     /**
862      * {@inheritDoc}
863      */
864     @Override
865     public String getLocalName() {
866         if (wasCreatedByJavascript()
867                 && (hasFeature(HTMLIMAGE_HTMLELEMENT) || hasFeature(HTMLIMAGE_HTMLUNKNOWNELEMENT))) {
868             return originalQualifiedName_;
869         }
870         return super.getLocalName();
871     }
872 
873     /**
874      * {@inheritDoc}
875      */
876     @Override
877     public ScriptResult fireEvent(final Event event) {
878         if (event instanceof MouseEvent) {
879             final MouseEvent mouseEvent = (MouseEvent) event;
880             if (lastClickX_ >= 0) {
881                 mouseEvent.setClientX(getPosX() + lastClickX_);
882             }
883             if (lastClickY_ >= 0) {
884                 mouseEvent.setClientY(getPosX() + lastClickY_);
885             }
886         }
887 
888         return super.fireEvent(event);
889     }
890 }