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