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.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36 import org.htmlunit.BrowserVersion;
37 import org.htmlunit.Page;
38 import org.htmlunit.ScriptResult;
39 import org.htmlunit.SgmlPage;
40 import org.htmlunit.WebClient;
41 import org.htmlunit.WebRequest;
42 import org.htmlunit.WebResponse;
43 import org.htmlunit.http.HttpStatus;
44 import org.htmlunit.javascript.AbstractJavaScriptEngine;
45 import org.htmlunit.javascript.PostponedAction;
46 import org.htmlunit.javascript.host.dom.Document;
47 import org.htmlunit.javascript.host.event.Event;
48 import org.htmlunit.javascript.host.event.MouseEvent;
49 import org.htmlunit.platform.Platform;
50 import org.htmlunit.platform.geom.IntDimension2D;
51 import org.htmlunit.platform.image.ImageData;
52 import org.htmlunit.util.StringUtils;
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 = 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 (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.isEmptyOrNull(src)) {
530 return 0;
531 }
532 if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)
533 && 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
550
551
552
553
554
555
556 public int getWidth() throws IOException {
557 if (width_ < 0) {
558 determineWidthAndHeight();
559 }
560 return width_;
561 }
562
563
564
565
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
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.isEmptyOrNull(src)) {
592 return 0;
593 }
594 if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)
595 && StringUtils.isBlank(src)) {
596 return 0;
597 }
598
599 try {
600 return getWidth();
601 }
602 catch (final IOException e) {
603 if (browserVersion.hasFeature(JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0)) {
604 return 16;
605 }
606 return 24;
607 }
608 }
609
610
611
612
613
614 public ImageData getImageData() throws IOException {
615 readImageIfNeeded();
616 return imageData_;
617 }
618
619 private void determineWidthAndHeight() throws IOException {
620 readImageIfNeeded();
621
622 final IntDimension2D dim = imageData_.getWidthHeight();
623 width_ = dim.getWidth();
624 height_ = dim.getHeight();
625
626
627
628 closeImageData();
629 }
630
631 private void closeImageData() throws IOException {
632 if (imageData_ != null) {
633 try {
634 imageData_.close();
635 }
636 catch (final IOException e) {
637 throw e;
638 }
639 catch (final Exception ex) {
640 throw new IOException("Exception during close()", ex);
641 }
642 imageData_ = null;
643 }
644 }
645
646
647
648
649
650
651
652
653
654
655
656
657 public WebResponse getWebResponse(final boolean downloadIfNeeded) throws IOException {
658 if (downloadIfNeeded) {
659 downloadImageIfNeeded();
660 }
661 return imageWebResponse_;
662 }
663
664
665
666
667
668
669
670
671 private void downloadImageIfNeeded() throws IOException {
672 if (!downloaded_) {
673
674 final String src = getSrcAttribute();
675
676 if (!StringUtils.isEmptyString(src)) {
677 final HtmlPage page = (HtmlPage) getPage();
678 final WebClient webClient = page.getWebClient();
679 final BrowserVersion browser = webClient.getBrowserVersion();
680
681 if (!(browser.hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY)
682 && StringUtils.isBlank(src))) {
683 final URL url = page.getFullyQualifiedUrl(src);
684 final WebRequest request = new WebRequest(url, browser.getImgAcceptHeader(),
685 browser.getAcceptEncodingHeader());
686 request.setCharset(page.getCharset());
687 request.setRefererHeader(page.getUrl());
688 imageWebResponse_ = webClient.loadWebResponse(request);
689 }
690 }
691
692 closeImageData();
693
694 downloaded_ = true;
695 isComplete_ = true;
696
697 width_ = -1;
698 height_ = -1;
699 }
700 }
701
702 private void readImageIfNeeded() throws IOException {
703 downloadImageIfNeeded();
704 if (imageData_ == null) {
705 if (null == imageWebResponse_) {
706 throw new IOException("No image response available (src='" + getSrcAttribute() + "')");
707 }
708 imageData_ = Platform.buildImageData(imageWebResponse_.getContentAsStream());
709 }
710 }
711
712
713
714
715
716
717
718
719
720
721
722
723 public Page click(final int x, final int y) throws IOException {
724 lastClickX_ = x;
725 lastClickY_ = y;
726 try {
727 return super.click();
728 }
729 finally {
730 lastClickX_ = -1;
731 lastClickY_ = -1;
732 }
733 }
734
735
736
737
738
739
740
741
742
743 @Override
744 @SuppressWarnings("unchecked")
745 public Page click() throws IOException {
746 return click(0, 0);
747 }
748
749
750
751
752
753
754 @Override
755 protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
756 if (ATTRIBUTE_NOT_DEFINED != getUseMapAttribute()) {
757
758 final String mapName = getUseMapAttribute().substring(1);
759 final HtmlElement doc = ((HtmlPage) getPage()).getDocumentElement();
760 final HtmlMap map = doc.getOneHtmlElementByAttribute("map", NAME_ATTRIBUTE, mapName);
761 for (final DomElement element : map.getChildElements()) {
762 if (element instanceof HtmlArea) {
763 final HtmlArea area = (HtmlArea) element;
764 if (area.containsPoint(Math.max(lastClickX_, 0), Math.max(lastClickY_, 0))) {
765 area.doClickStateUpdate(shiftKey, ctrlKey);
766 return false;
767 }
768 }
769 }
770 }
771 final HtmlAnchor anchor = (HtmlAnchor) getEnclosingElement("a");
772 if (anchor == null) {
773 return false;
774 }
775 if (ATTRIBUTE_NOT_DEFINED != getIsmapAttribute()) {
776 final String suffix = "?" + Math.max(lastClickX_, 0) + "," + Math.max(lastClickY_, 0);
777 anchor.doClickStateUpdate(false, false, suffix);
778 return false;
779 }
780 anchor.doClickStateUpdate(shiftKey, ctrlKey);
781 return false;
782 }
783
784
785
786
787
788
789 public void saveAs(final File file) throws IOException {
790 downloadImageIfNeeded();
791 if (null != imageWebResponse_) {
792 try (OutputStream fos = Files.newOutputStream(file.toPath());
793 InputStream inputStream = imageWebResponse_.getContentAsStream()) {
794 IOUtils.copy(inputStream, fos);
795 }
796 }
797 }
798
799
800
801
802 @Override
803 public DisplayStyle getDefaultStyleDisplay() {
804 return DisplayStyle.INLINE;
805 }
806
807
808
809
810 public boolean isComplete() {
811 return isComplete_ || ATTRIBUTE_NOT_DEFINED == getSrcAttribute();
812 }
813
814
815
816
817 @Override
818 public boolean isDisplayed() {
819 final String src = getSrcAttribute();
820 if (ATTRIBUTE_NOT_DEFINED == src) {
821 return false;
822 }
823 if (hasFeature(HTMLIMAGE_BLANK_SRC_AS_EMPTY) && StringUtils.isBlank(src)) {
824 return false;
825 }
826 if (hasFeature(HTMLIMAGE_EMPTY_SRC_DISPLAY_FALSE) && StringUtils.isEmptyOrNull(src)) {
827 return false;
828 }
829
830 return super.isDisplayed();
831 }
832
833
834
835
836
837
838 public void markAsCreatedByJavascript() {
839 createdByJavascript_ = true;
840 }
841
842
843
844
845
846
847
848 public boolean wasCreatedByJavascript() {
849 return createdByJavascript_;
850 }
851
852
853
854
855
856
857 public String getOriginalQualifiedName() {
858 return originalQualifiedName_;
859 }
860
861
862
863
864 @Override
865 public String getLocalName() {
866 if (wasCreatedByJavascript()
867 && (hasFeature(HTMLIMAGE_HTMLELEMENT) || hasFeature(HTMLIMAGE_HTMLUNKNOWNELEMENT))) {
868 return originalQualifiedName_;
869 }
870 return super.getLocalName();
871 }
872
873
874
875
876 @Override
877 public ScriptResult fireEvent(final Event event) {
878 if (event instanceof MouseEvent) {
879 final MouseEvent mouseEvent = (MouseEvent) event;
880 if (lastClickX_ >= 0) {
881 mouseEvent.setClientX(getPosX() + lastClickX_);
882 }
883 if (lastClickY_ >= 0) {
884 mouseEvent.setClientY(getPosX() + lastClickY_);
885 }
886 }
887
888 return super.fireEvent(event);
889 }
890 }