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