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