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.lang3.StringUtils;
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.htmlunit.BrowserVersion;
38  import org.htmlunit.Page;
39  import org.htmlunit.ScriptResult;
40  import org.htmlunit.SgmlPage;
41  import org.htmlunit.WebClient;
42  import org.htmlunit.WebRequest;
43  import org.htmlunit.WebResponse;
44  import org.htmlunit.http.HttpStatus;
45  import org.htmlunit.javascript.AbstractJavaScriptEngine;
46  import org.htmlunit.javascript.PostponedAction;
47  import org.htmlunit.javascript.host.dom.Document;
48  import org.htmlunit.javascript.host.event.Event;
49  import org.htmlunit.javascript.host.event.MouseEvent;
50  import org.htmlunit.platform.Platform;
51  import org.htmlunit.platform.geom.IntDimension2D;
52  import org.htmlunit.platform.image.ImageData;
53  import org.htmlunit.util.UrlUtils;
54  
55  /**
56   * Wrapper for the HTML element "img".
57   *
58   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
59   * @author David K. Taylor
60   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
61   * @author Ahmed Ashour
62   * @author <a href="mailto:knut.johannes.dahle@gmail.com">Knut Johannes Dahle</a>
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 = org.htmlunit.util.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 (org.htmlunit.util.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.isEmpty(src)) {
530             return 0;
531         }
532         if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0) && StringUtils.isBlank(src)) {
533             return 0;
534         }
535 
536         try {
537             return getHeight();
538         }
539         catch (final IOException e) {
540             if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)) {
541                 return 16;
542             }
543             return 24;
544         }
545     }
546 
547     /**
548      * <p>Returns the image's actual width (<b>not</b> the image's {@link #getWidthAttribute() width attribute}).</p>
549      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
550      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
551      *
552      * @return the image's actual width
553      * @throws IOException if an error occurs while downloading or reading the image
554      */
555     public int getWidth() throws IOException {
556         if (width_ < 0) {
557             determineWidthAndHeight();
558         }
559         return width_;
560     }
561 
562     /**
563      * Returns the value same value as the js width property.
564      * @return the value of the {@code width} property
565      */
566     public int getWidthOrDefault() {
567         final String widthAttrib = getWidthAttribute();
568 
569         if (ATTRIBUTE_NOT_DEFINED != widthAttrib) {
570             try {
571                 return Integer.parseInt(widthAttrib);
572             }
573             catch (final NumberFormatException ignored) {
574                 // ignore
575             }
576         }
577 
578         final String src = getSrcAttribute();
579         if (ATTRIBUTE_NOT_DEFINED == src) {
580             final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion();
581             if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)
582                     || browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_24x24_0x0)) {
583                 return 0;
584             }
585             return 24;
586         }
587 
588         final WebClient webClient = getPage().getWebClient();
589         final BrowserVersion browserVersion = webClient.getBrowserVersion();
590         if (StringUtils.isEmpty(src)) {
591             return 0;
592         }
593         if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0) && StringUtils.isBlank(src)) {
594             return 0;
595         }
596 
597         try {
598             return getWidth();
599         }
600         catch (final IOException e) {
601             if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)) {
602                 return 16;
603             }
604             return 24;
605         }
606     }
607 
608     /**
609      * @return the {@link ImageData} of this image
610      * @throws IOException in case of error
611      */
612     public ImageData getImageData() throws IOException {
613         readImageIfNeeded();
614         return imageData_;
615     }
616 
617     private void determineWidthAndHeight() throws IOException {
618         readImageIfNeeded();
619 
620         final IntDimension2D dim = imageData_.getWidthHeight();
621         width_ = dim.getWidth();
622         height_ = dim.getHeight();
623 
624         // ImageIO creates temp files; to save file handles
625         // we will cache the values and close this directly to free the resources
626         closeImageData();
627     }
628 
629     private void closeImageData() throws IOException {
630         if (imageData_ != null) {
631             try {
632                 imageData_.close();
633             }
634             catch (final IOException e) {
635                 throw e;
636             }
637             catch (final Exception ex) {
638                 throw new IOException("Exception during close()", ex);
639             }
640             imageData_ = null;
641         }
642     }
643 
644     /**
645      * <p>Returns the <code>WebResponse</code> for the image contained by this image element.</p>
646      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
647      * <p>If the image has not already been downloaded and <code>downloadIfNeeded</code> is {@code true}, this method
648      * triggers a download and caches the image.</p>
649      *
650      * @param downloadIfNeeded whether or not the image should be downloaded (if it hasn't already been downloaded)
651      * @return {@code null} if no download should be performed and one hasn't already been triggered; otherwise,
652      *         the response received when performing a request for the image referenced by this element
653      * @throws IOException if an error occurs while downloading the image
654      */
655     public WebResponse getWebResponse(final boolean downloadIfNeeded) throws IOException {
656         if (downloadIfNeeded) {
657             downloadImageIfNeeded();
658         }
659         return imageWebResponse_;
660     }
661 
662     /**
663      * <p>Downloads the image contained by this image element.</p>
664      * <p><span style="color:red">POTENTIAL PERFORMANCE KILLER - DOWNLOADS THE IMAGE - USE AT YOUR OWN RISK</span></p>
665      * <p>If the image has not already been downloaded, this method triggers a download and caches the image.</p>
666      *
667      * @throws IOException if an error occurs while downloading the image
668      */
669     private void downloadImageIfNeeded() throws IOException {
670         if (!downloaded_) {
671             // HTMLIMAGE_BLANK_SRC_AS_EMPTY
672             final String src = getSrcAttribute();
673 
674             if (!org.htmlunit.util.StringUtils.isEmptyString(src)) {
675                 final HtmlPage page = (HtmlPage) getPage();
676                 final WebClient webClient = page.getWebClient();
677                 final BrowserVersion browser = webClient.getBrowserVersion();
678 
679                 if (!(browser.hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY)
680                         && StringUtils.isBlank(src))) {
681                     final URL url = page.getFullyQualifiedUrl(src);
682                     final WebRequest request = new WebRequest(url, browser.getImgAcceptHeader(),
683                                                                     browser.getAcceptEncodingHeader());
684                     request.setCharset(page.getCharset());
685                     request.setRefererHeader(page.getUrl());
686                     imageWebResponse_ = webClient.loadWebResponse(request);
687                 }
688             }
689 
690             closeImageData();
691 
692             downloaded_ = true;
693             isComplete_ = true;
694 
695             width_ = -1;
696             height_ = -1;
697         }
698     }
699 
700     private void readImageIfNeeded() throws IOException {
701         downloadImageIfNeeded();
702         if (imageData_ == null) {
703             if (null == imageWebResponse_) {
704                 throw new IOException("No image response available (src='" + getSrcAttribute() + "')");
705             }
706             imageData_ = Platform.buildImageData(imageWebResponse_.getContentAsStream());
707         }
708     }
709 
710     /**
711      * Simulates clicking this element at the specified position. This only makes sense for
712      * an image map (currently only server side), where the position matters. This method
713      * returns the page contained by this image's window after the click, which may or may not
714      * be the same as the original page, depending on JavaScript event handlers, etc.
715      *
716      * @param x the x position of the click
717      * @param y the y position of the click
718      * @return the page contained by this image's window after the click
719      * @exception IOException if an IO error occurs
720      */
721     public Page click(final int x, final int y) throws IOException {
722         lastClickX_ = x;
723         lastClickY_ = y;
724         try {
725             return super.click();
726         }
727         finally {
728             lastClickX_ = -1;
729             lastClickY_ = -1;
730         }
731     }
732 
733     /**
734      * Simulates clicking this element at the position <code>(0, 0)</code>. This method returns
735      * the page contained by this image's window after the click, which may or may not be the
736      * same as the original page, depending on JavaScript event handlers, etc.
737      *
738      * @return the page contained by this image's window after the click
739      * @exception IOException if an IO error occurs
740      */
741     @Override
742     @SuppressWarnings("unchecked")
743     public Page click() throws IOException {
744         return click(0, 0);
745     }
746 
747     /**
748      * Performs the click action on the enclosing A tag (if any).
749      * {@inheritDoc}
750      * @throws IOException if an IO error occurred
751      */
752     @Override
753     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
754         if (ATTRIBUTE_NOT_DEFINED != getUseMapAttribute()) {
755             // remove initial '#'
756             final String mapName = getUseMapAttribute().substring(1);
757             final HtmlElement doc = ((HtmlPage) getPage()).getDocumentElement();
758             final HtmlMap map = doc.getOneHtmlElementByAttribute("map", NAME_ATTRIBUTE, mapName);
759             for (final DomElement element : map.getChildElements()) {
760                 if (element instanceof HtmlArea) {
761                     final HtmlArea area = (HtmlArea) element;
762                     if (area.containsPoint(Math.max(lastClickX_, 0), Math.max(lastClickY_, 0))) {
763                         area.doClickStateUpdate(shiftKey, ctrlKey);
764                         return false;
765                     }
766                 }
767             }
768         }
769         final HtmlAnchor anchor = (HtmlAnchor) getEnclosingElement("a");
770         if (anchor == null) {
771             return false;
772         }
773         if (ATTRIBUTE_NOT_DEFINED != getIsmapAttribute()) {
774             final String suffix = "?" + Math.max(lastClickX_, 0) + "," + Math.max(lastClickY_, 0);
775             anchor.doClickStateUpdate(false, false, suffix);
776             return false;
777         }
778         anchor.doClickStateUpdate(shiftKey, ctrlKey);
779         return false;
780     }
781 
782     /**
783      * Saves this image as the specified file.
784      * @param file the file to save to
785      * @throws IOException if an IO error occurs
786      */
787     public void saveAs(final File file) throws IOException {
788         downloadImageIfNeeded();
789         if (null != imageWebResponse_) {
790             try (OutputStream fos = Files.newOutputStream(file.toPath());
791                     InputStream inputStream = imageWebResponse_.getContentAsStream()) {
792                 IOUtils.copy(inputStream, fos);
793             }
794         }
795     }
796 
797     /**
798      * {@inheritDoc}
799      */
800     @Override
801     public DisplayStyle getDefaultStyleDisplay() {
802         return DisplayStyle.INLINE;
803     }
804 
805     /**
806      * @return true if the image was successfully downloaded
807      */
808     public boolean isComplete() {
809         return isComplete_ || ATTRIBUTE_NOT_DEFINED == getSrcAttribute();
810     }
811 
812     /**
813      * {@inheritDoc}
814      */
815     @Override
816     public boolean isDisplayed() {
817         final String src = getSrcAttribute();
818         if (ATTRIBUTE_NOT_DEFINED == src) {
819             return false;
820         }
821         if (hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY) && StringUtils.isBlank(src)) {
822             return false;
823         }
824         if (hasFeature(HTMLIMAGE_EMPTY_SRC_DISPLAY_FALSE) && StringUtils.isEmpty(src)) {
825             return false;
826         }
827 
828         return super.isDisplayed();
829     }
830 
831     /**
832      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
833      *
834      * Marks this frame as created by javascript.
835      */
836     public void markAsCreatedByJavascript() {
837         createdByJavascript_ = true;
838     }
839 
840     /**
841      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
842      *
843      * Returns true if this frame was created by javascript.
844      * @return true or false
845      */
846     public boolean wasCreatedByJavascript() {
847         return createdByJavascript_;
848     }
849 
850     /**
851      * Returns the original element qualified name,
852      * this is needed to differentiate between <code>img</code> and <code>image</code>.
853      * @return the original element qualified name
854      */
855     public String getOriginalQualifiedName() {
856         return originalQualifiedName_;
857     }
858 
859     /**
860      * {@inheritDoc}
861      */
862     @Override
863     public String getLocalName() {
864         if (wasCreatedByJavascript()
865                 && (hasFeature(HTMLIMAGE_HTMLELEMENT) || hasFeature(HTMLIMAGE_HTMLUNKNOWNELEMENT))) {
866             return originalQualifiedName_;
867         }
868         return super.getLocalName();
869     }
870 
871     /**
872      * {@inheritDoc}
873      */
874     @Override
875     public ScriptResult fireEvent(final Event event) {
876         if (event instanceof MouseEvent) {
877             final MouseEvent mouseEvent = (MouseEvent) event;
878             if (lastClickX_ >= 0) {
879                 mouseEvent.setClientX(getPosX() + lastClickX_);
880             }
881             if (lastClickY_ >= 0) {
882                 mouseEvent.setClientY(getPosX() + lastClickY_);
883             }
884         }
885 
886         return super.fireEvent(event);
887     }
888 }