1
2
3
4
5
6
7
8
9
10
11
12
13
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
59
60
61
62
63
64
65
66
67
68
69
70 public class HtmlImage extends HtmlElement {
71
72 private static final Log LOG = LogFactory.getLog(HtmlImage.class);
73
74
75 public static final String TAG_NAME = "img";
76
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
94
95
96
97
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
124
125 @Override
126 protected void onAddedToPage() {
127 doOnLoad();
128 super.onAddedToPage();
129 }
130
131
132
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
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
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
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
207 }
208
209 if (oldUrl == null || !UrlUtils.sameFile(oldUrl, url)) {
210
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;
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254 public void doOnLoad() {
255 if (onloadProcessed_) {
256 return;
257 }
258
259 final HtmlPage htmlPage = getHtmlPageOrNull();
260 if (htmlPage == null) {
261 return;
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
279 try {
280 downloadImageIfNeeded();
281
282 if (imageWebResponse_.isSuccess()) {
283 if (imageWebResponse_.getStatusCode() != HttpStatus.NO_CONTENT_204) {
284 loadSuccessful = true;
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
339
340
341
342
343
344 public final String getSrcAttribute() {
345 return getSrcAttributeNormalized();
346 }
347
348
349
350
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
369
370
371
372
373
374 public final String getAltAttribute() {
375 return getAttributeDirect("alt");
376 }
377
378
379
380
381
382
383
384
385 public final String getNameAttribute() {
386 return getAttributeDirect(NAME_ATTRIBUTE);
387 }
388
389
390
391
392
393
394
395
396 public final String getLongDescAttribute() {
397 return getAttributeDirect("longdesc");
398 }
399
400
401
402
403
404
405
406
407 public final String getHeightAttribute() {
408 return getAttributeDirect("height");
409 }
410
411
412
413
414
415
416
417
418 public final String getWidthAttribute() {
419 return getAttributeDirect("width");
420 }
421
422
423
424
425
426
427
428
429 public final String getUseMapAttribute() {
430 return getAttributeDirect("usemap");
431 }
432
433
434
435
436
437
438
439
440 public final String getIsmapAttribute() {
441 return getAttributeDirect("ismap");
442 }
443
444
445
446
447
448
449
450
451 public final String getAlignAttribute() {
452 return getAttributeDirect("align");
453 }
454
455
456
457
458
459
460
461
462 public final String getBorderAttribute() {
463 return getAttributeDirect("border");
464 }
465
466
467
468
469
470
471
472
473 public final String getHspaceAttribute() {
474 return getAttributeDirect("hspace");
475 }
476
477
478
479
480
481
482
483
484 public final String getVspaceAttribute() {
485 return getAttributeDirect("vspace");
486 }
487
488
489
490
491
492
493
494
495
496 public int getHeight() throws IOException {
497 if (height_ < 0) {
498 determineWidthAndHeight();
499 }
500 return height_;
501 }
502
503
504
505
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
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
552
553
554
555
556
557
558 public int getWidth() throws IOException {
559 if (width_ < 0) {
560 determineWidthAndHeight();
561 }
562 return width_;
563 }
564
565
566
567
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
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
614
615
616
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
631
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
652
653
654
655
656
657
658
659
660
661 public WebResponse getWebResponse(final boolean downloadIfNeeded) throws IOException {
662 if (downloadIfNeeded) {
663 downloadImageIfNeeded();
664 }
665 return imageWebResponse_;
666 }
667
668
669
670
671
672
673
674
675 private void downloadImageIfNeeded() throws IOException {
676 if (!downloaded_) {
677
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
694
695
696
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
736
737
738
739
740
741
742
743
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
759
760
761
762
763
764
765 @Override
766 @SuppressWarnings("unchecked")
767 public Page click() throws IOException {
768 return click(0, 0);
769 }
770
771
772
773
774
775
776 @Override
777 protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
778 if (ATTRIBUTE_NOT_DEFINED != getUseMapAttribute()) {
779
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
807
808
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
822
823 @Override
824 public DisplayStyle getDefaultStyleDisplay() {
825 return DisplayStyle.INLINE;
826 }
827
828
829
830
831
832
833
834 public boolean isComplete() {
835 return isComplete_ || ATTRIBUTE_NOT_DEFINED == getSrcAttribute();
836 }
837
838
839
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
859
860
861
862 public void markAsCreatedByJavascript() {
863 createdByJavascript_ = true;
864 }
865
866
867
868
869
870
871
872 public boolean wasCreatedByJavascript() {
873 return createdByJavascript_;
874 }
875
876
877
878
879
880
881 public String getOriginalQualifiedName() {
882 return originalQualifiedName_;
883 }
884
885
886
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
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 }