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