Picture element needs to respond to dynamic viewport changes.
[WebKit-https.git] / Source / WebCore / html / HTMLImageElement.cpp
1 /*
2  * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3  *           (C) 1999 Antti Koivisto (koivisto@kde.org)
4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2010 Apple Inc. All rights reserved.
5  * Copyright (C) 2010 Google Inc. All rights reserved.
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public License
18  * along with this library; see the file COPYING.LIB.  If not, write to
19  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20  * Boston, MA 02110-1301, USA.
21  */
22
23 #include "config.h"
24 #include "HTMLImageElement.h"
25
26 #include "CSSPropertyNames.h"
27 #include "CSSValueKeywords.h"
28 #include "CachedImage.h"
29 #include "EventNames.h"
30 #include "FrameView.h"
31 #include "HTMLAnchorElement.h"
32 #include "HTMLDocument.h"
33 #include "HTMLFormElement.h"
34 #include "HTMLParserIdioms.h"
35 #include "HTMLPictureElement.h"
36 #include "HTMLSourceElement.h"
37 #include "HTMLSrcsetParser.h"
38 #include "MIMETypeRegistry.h"
39 #include "MediaList.h"
40 #include "MediaQueryEvaluator.h"
41 #include "Page.h"
42 #include "RenderImage.h"
43 #include "Settings.h"
44 #include "ShadowRoot.h"
45 #include "SourceSizeList.h"
46 #include <wtf/text/StringBuilder.h>
47
48 #if ENABLE(SERVICE_CONTROLS)
49 #include "ImageControlsRootElement.h"
50 #endif
51
52 namespace WebCore {
53
54 using namespace HTMLNames;
55
56 HTMLImageElement::HTMLImageElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
57     : HTMLElement(tagName, document)
58     , m_imageLoader(*this)
59     , m_form(nullptr)
60     , m_formSetByParser(form)
61     , m_compositeOperator(CompositeSourceOver)
62     , m_imageDevicePixelRatio(1.0f)
63 #if ENABLE(SERVICE_CONTROLS)
64     , m_experimentalImageMenuEnabled(false)
65 #endif
66 {
67     ASSERT(hasTagName(imgTag));
68     setHasCustomStyleResolveCallbacks();
69 }
70
71 Ref<HTMLImageElement> HTMLImageElement::create(Document& document)
72 {
73     return adoptRef(*new HTMLImageElement(imgTag, document));
74 }
75
76 Ref<HTMLImageElement> HTMLImageElement::create(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
77 {
78     return adoptRef(*new HTMLImageElement(tagName, document, form));
79 }
80
81 HTMLImageElement::~HTMLImageElement()
82 {
83     if (m_form)
84         m_form->removeImgElement(this);
85 }
86
87 Ref<HTMLImageElement> HTMLImageElement::createForJSConstructor(Document& document, const int* optionalWidth, const int* optionalHeight)
88 {
89     Ref<HTMLImageElement> image = adoptRef(*new HTMLImageElement(imgTag, document));
90     if (optionalWidth)
91         image->setWidth(*optionalWidth);
92     if (optionalHeight)
93         image->setHeight(*optionalHeight);
94     return image;
95 }
96
97 bool HTMLImageElement::isPresentationAttribute(const QualifiedName& name) const
98 {
99     if (name == widthAttr || name == heightAttr || name == borderAttr || name == vspaceAttr || name == hspaceAttr || name == alignAttr || name == valignAttr)
100         return true;
101     return HTMLElement::isPresentationAttribute(name);
102 }
103
104 void HTMLImageElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style)
105 {
106     if (name == widthAttr)
107         addHTMLLengthToStyle(style, CSSPropertyWidth, value);
108     else if (name == heightAttr)
109         addHTMLLengthToStyle(style, CSSPropertyHeight, value);
110     else if (name == borderAttr)
111         applyBorderAttributeToStyle(value, style);
112     else if (name == vspaceAttr) {
113         addHTMLLengthToStyle(style, CSSPropertyMarginTop, value);
114         addHTMLLengthToStyle(style, CSSPropertyMarginBottom, value);
115     } else if (name == hspaceAttr) {
116         addHTMLLengthToStyle(style, CSSPropertyMarginLeft, value);
117         addHTMLLengthToStyle(style, CSSPropertyMarginRight, value);
118     } else if (name == alignAttr)
119         applyAlignmentAttributeToStyle(value, style);
120     else if (name == valignAttr)
121         addPropertyToPresentationAttributeStyle(style, CSSPropertyVerticalAlign, value);
122     else
123         HTMLElement::collectStyleForPresentationAttribute(name, value, style);
124 }
125
126 const AtomicString& HTMLImageElement::imageSourceURL() const
127 {
128     return m_bestFitImageURL.isEmpty() ? fastGetAttribute(srcAttr) : m_bestFitImageURL;
129 }
130
131 void HTMLImageElement::setBestFitURLAndDPRFromImageCandidate(const ImageCandidate& candidate)
132 {
133     m_bestFitImageURL = candidate.string.toString();
134     m_currentSrc = AtomicString(document().completeURL(imageSourceURL()).string());
135     if (candidate.density >= 0)
136         m_imageDevicePixelRatio = 1 / candidate.density;
137     if (is<RenderImage>(renderer()))
138         downcast<RenderImage>(*renderer()).setImageDevicePixelRatio(m_imageDevicePixelRatio);
139 }
140
141 ImageCandidate HTMLImageElement::bestFitSourceFromPictureElement()
142 {
143     auto* parent = parentNode();
144     if (!is<HTMLPictureElement>(parent))
145         return { };
146     auto* picture = downcast<HTMLPictureElement>(parent);
147     picture->clearViewportDependentResults();
148     document().removeViewportDependentPicture(*picture);
149     for (Node* child = parent->firstChild(); child && child != this; child = child->nextSibling()) {
150         if (!is<HTMLSourceElement>(*child))
151             continue;
152         auto& source = downcast<HTMLSourceElement>(*child);
153         auto& srcset = source.fastGetAttribute(srcsetAttr);
154         if (srcset.isEmpty())
155             continue;
156         if (source.hasAttribute(typeAttr)) {
157             String type = source.fastGetAttribute(typeAttr).string();
158             int indexOfSemicolon = type.find(';');
159             if (indexOfSemicolon >= 0)
160                 type.truncate(indexOfSemicolon);
161             type = stripLeadingAndTrailingHTMLSpaces(type);
162             type = type.lower();
163             if (!type.isEmpty() && !MIMETypeRegistry::isSupportedImageMIMEType(type) && type != "image/svg+xml")
164                 continue;
165         }
166         MediaQueryEvaluator evaluator(document().printing() ? "print" : "screen", document().frame(), computedStyle());
167         bool evaluation = evaluator.evalCheckingViewportDependentResults(source.mediaQuerySet(), picture->viewportDependentResults());
168         if (picture->hasViewportDependentResults())
169             document().addViewportDependentPicture(*picture);
170         if (!evaluation)
171             continue;
172
173         float sourceSize = parseSizesAttribute(source.fastGetAttribute(sizesAttr).string(), document().renderView(), document().frame());
174         ImageCandidate candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), nullAtom, source.fastGetAttribute(srcsetAttr), sourceSize);
175         if (!candidate.isEmpty())
176             return candidate;
177     }
178     return { };
179 }
180
181 void HTMLImageElement::selectImageSource()
182 {
183     // First look for the best fit source from our <picture> parent if we have one.
184     ImageCandidate candidate = bestFitSourceFromPictureElement();
185     if (candidate.isEmpty()) {
186         // If we don't have a <picture> or didn't find a source, then we use our own attributes.
187         float sourceSize = parseSizesAttribute(fastGetAttribute(sizesAttr).string(), document().renderView(), document().frame());
188         candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), fastGetAttribute(srcAttr), fastGetAttribute(srcsetAttr), sourceSize);
189     }
190     setBestFitURLAndDPRFromImageCandidate(candidate);
191     m_imageLoader.updateFromElementIgnoringPreviousError();
192 }
193
194 void HTMLImageElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
195 {
196     if (name == altAttr) {
197         if (is<RenderImage>(renderer()))
198             downcast<RenderImage>(*renderer()).updateAltText();
199     } else if (name == srcAttr || name == srcsetAttr || name == sizesAttr)
200         selectImageSource();
201     else if (name == usemapAttr) {
202         if (inDocument() && !m_lowercasedUsemap.isNull())
203             document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
204
205         // The HTMLImageElement's useMap() value includes the '#' symbol at the beginning, which has to be stripped off.
206         // FIXME: We should check that the first character is '#'.
207         // FIXME: HTML5 specification says we should strip any leading string before '#'.
208         // FIXME: HTML5 specification says we should ignore usemap attributes without #.
209         if (value.length() > 1)
210             m_lowercasedUsemap = value.string().substring(1).lower();
211         else
212             m_lowercasedUsemap = nullAtom;
213
214         if (inDocument() && !m_lowercasedUsemap.isNull())
215             document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
216     } else if (name == compositeAttr) {
217         // FIXME: images don't support blend modes in their compositing attribute.
218         BlendMode blendOp = BlendModeNormal;
219         if (!parseCompositeAndBlendOperator(value, m_compositeOperator, blendOp))
220             m_compositeOperator = CompositeSourceOver;
221 #if ENABLE(SERVICE_CONTROLS)
222     } else if (name == webkitimagemenuAttr) {
223         m_experimentalImageMenuEnabled = !value.isNull();
224         updateImageControls();
225 #endif
226     } else {
227         if (name == nameAttr) {
228             bool willHaveName = !value.isNull();
229             if (m_hadNameBeforeAttributeChanged != willHaveName && inDocument() && is<HTMLDocument>(document())) {
230                 HTMLDocument& document = downcast<HTMLDocument>(this->document());
231                 const AtomicString& id = getIdAttribute();
232                 if (!id.isEmpty() && id != getNameAttribute()) {
233                     if (willHaveName)
234                         document.addDocumentNamedItem(*id.impl(), *this);
235                     else
236                         document.removeDocumentNamedItem(*id.impl(), *this);
237                 }
238             }
239             m_hadNameBeforeAttributeChanged = willHaveName;
240         }
241         HTMLElement::parseAttribute(name, value);
242     }
243 }
244
245 const AtomicString& HTMLImageElement::altText() const
246 {
247     // lets figure out the alt text.. magic stuff
248     // http://www.w3.org/TR/1998/REC-html40-19980424/appendix/notes.html#altgen
249     // also heavily discussed by Hixie on bugzilla
250     const AtomicString& alt = fastGetAttribute(altAttr);
251     if (!alt.isNull())
252         return alt;
253     // fall back to title attribute
254     return fastGetAttribute(titleAttr);
255 }
256
257 RenderPtr<RenderElement> HTMLImageElement::createElementRenderer(Ref<RenderStyle>&& style, const RenderTreePosition&)
258 {
259     if (style.get().hasContent())
260         return RenderElement::createFor(*this, WTF::move(style));
261
262     return createRenderer<RenderImage>(*this, WTF::move(style), nullptr, m_imageDevicePixelRatio);
263 }
264
265 bool HTMLImageElement::canStartSelection() const
266 {
267     if (shadowRoot())
268         return HTMLElement::canStartSelection();
269
270     return false;
271 }
272
273 void HTMLImageElement::didAttachRenderers()
274 {
275     if (!is<RenderImage>(renderer()))
276         return;
277     if (m_imageLoader.hasPendingBeforeLoadEvent())
278         return;
279
280 #if ENABLE(SERVICE_CONTROLS)
281     updateImageControls();
282 #endif
283
284     auto& renderImage = downcast<RenderImage>(*renderer());
285     RenderImageResource& renderImageResource = renderImage.imageResource();
286     if (renderImageResource.hasImage())
287         return;
288     renderImageResource.setCachedImage(m_imageLoader.image());
289
290     // If we have no image at all because we have no src attribute, set
291     // image height and width for the alt text instead.
292     if (!m_imageLoader.image() && !renderImageResource.cachedImage())
293         renderImage.setImageSizeForAltText();
294 }
295
296 Node::InsertionNotificationRequest HTMLImageElement::insertedInto(ContainerNode& insertionPoint)
297 {
298     if (m_formSetByParser) {
299         m_form = m_formSetByParser;
300         m_formSetByParser = nullptr;
301     }
302
303     if (!m_form)
304         m_form = HTMLFormElement::findClosestFormAncestor(*this);
305
306     if (m_form)
307         m_form->registerImgElement(this);
308
309     // Insert needs to complete first, before we start updating the loader. Loader dispatches events which could result
310     // in callbacks back to this node.
311     Node::InsertionNotificationRequest insertNotificationRequest = HTMLElement::insertedInto(insertionPoint);
312
313     if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
314         document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
315
316     if (is<HTMLPictureElement>(parentNode()))
317         selectImageSource();
318
319     // If we have been inserted from a renderer-less document,
320     // our loader may have not fetched the image, so do it now.
321     if (insertionPoint.inDocument() && !m_imageLoader.image())
322         m_imageLoader.updateFromElement();
323
324     return insertNotificationRequest;
325 }
326
327 void HTMLImageElement::removedFrom(ContainerNode& insertionPoint)
328 {
329     if (m_form)
330         m_form->removeImgElement(this);
331
332     if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
333         document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
334
335     m_form = nullptr;
336     HTMLElement::removedFrom(insertionPoint);
337 }
338
339 int HTMLImageElement::width(bool ignorePendingStylesheets)
340 {
341     if (!renderer()) {
342         // check the attribute first for an explicit pixel value
343         bool ok;
344         int width = getAttribute(widthAttr).toInt(&ok);
345         if (ok)
346             return width;
347
348         // if the image is available, use its width
349         if (m_imageLoader.image())
350             return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
351     }
352
353     if (ignorePendingStylesheets)
354         document().updateLayoutIgnorePendingStylesheets();
355     else
356         document().updateLayout();
357
358     RenderBox* box = renderBox();
359     if (!box)
360         return 0;
361     LayoutRect contentRect = box->contentBoxRect();
362     return adjustForAbsoluteZoom(snappedIntRect(contentRect).width(), *box);
363 }
364
365 int HTMLImageElement::height(bool ignorePendingStylesheets)
366 {
367     if (!renderer()) {
368         // check the attribute first for an explicit pixel value
369         bool ok;
370         int height = getAttribute(heightAttr).toInt(&ok);
371         if (ok)
372             return height;
373
374         // if the image is available, use its height
375         if (m_imageLoader.image())
376             return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
377     }
378
379     if (ignorePendingStylesheets)
380         document().updateLayoutIgnorePendingStylesheets();
381     else
382         document().updateLayout();
383
384     RenderBox* box = renderBox();
385     if (!box)
386         return 0;
387     LayoutRect contentRect = box->contentBoxRect();
388     return adjustForAbsoluteZoom(snappedIntRect(contentRect).height(), *box);
389 }
390
391 int HTMLImageElement::naturalWidth() const
392 {
393     if (!m_imageLoader.image())
394         return 0;
395
396     return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
397 }
398
399 int HTMLImageElement::naturalHeight() const
400 {
401     if (!m_imageLoader.image())
402         return 0;
403
404     return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
405 }
406
407 bool HTMLImageElement::isURLAttribute(const Attribute& attribute) const
408 {
409     return attribute.name() == srcAttr
410         || attribute.name() == lowsrcAttr
411         || attribute.name() == longdescAttr
412         || (attribute.name() == usemapAttr && attribute.value().string()[0] != '#')
413         || HTMLElement::isURLAttribute(attribute);
414 }
415
416 bool HTMLImageElement::attributeContainsURL(const Attribute& attribute) const
417 {
418     return attribute.name() == srcsetAttr
419         || HTMLElement::attributeContainsURL(attribute);
420 }
421
422 String HTMLImageElement::completeURLsInAttributeValue(const URL& base, const Attribute& attribute) const
423 {
424     if (attribute.name() == srcsetAttr) {
425         Vector<ImageCandidate> imageCandidates = parseImageCandidatesFromSrcsetAttribute(StringView(attribute.value()));
426         StringBuilder result;
427         for (const auto& candidate : imageCandidates) {
428             if (&candidate != &imageCandidates[0])
429                 result.appendLiteral(", ");
430             result.append(URL(base, candidate.string.toString()).string());
431             if (candidate.density != UninitializedDescriptor) {
432                 result.append(' ');
433                 result.appendNumber(candidate.density);
434                 result.append('x');
435             }
436             if (candidate.resourceWidth != UninitializedDescriptor) {
437                 result.append(' ');
438                 result.appendNumber(candidate.resourceWidth);
439                 result.append('x');
440             }
441         }
442         return result.toString();
443     }
444     return HTMLElement::completeURLsInAttributeValue(base, attribute);
445 }
446
447 bool HTMLImageElement::matchesLowercasedUsemap(const AtomicStringImpl& name) const
448 {
449     ASSERT(String(&const_cast<AtomicStringImpl&>(name)).lower().impl() == &name);
450     return m_lowercasedUsemap.impl() == &name;
451 }
452
453 const AtomicString& HTMLImageElement::alt() const
454 {
455     return fastGetAttribute(altAttr);
456 }
457
458 bool HTMLImageElement::draggable() const
459 {
460     // Image elements are draggable by default.
461     return !equalIgnoringCase(fastGetAttribute(draggableAttr), "false");
462 }
463
464 void HTMLImageElement::setHeight(int value)
465 {
466     setIntegralAttribute(heightAttr, value);
467 }
468
469 URL HTMLImageElement::src() const
470 {
471     return document().completeURL(fastGetAttribute(srcAttr));
472 }
473
474 void HTMLImageElement::setSrc(const String& value)
475 {
476     setAttribute(srcAttr, value);
477 }
478
479 void HTMLImageElement::setWidth(int value)
480 {
481     setIntegralAttribute(widthAttr, value);
482 }
483
484 int HTMLImageElement::x() const
485 {
486     document().updateLayoutIgnorePendingStylesheets();
487     auto renderer = this->renderer();
488     if (!renderer)
489         return 0;
490
491     // FIXME: This doesn't work correctly with transforms.
492     return renderer->localToAbsolute().x();
493 }
494
495 int HTMLImageElement::y() const
496 {
497     document().updateLayoutIgnorePendingStylesheets();
498     auto renderer = this->renderer();
499     if (!renderer)
500         return 0;
501
502     // FIXME: This doesn't work correctly with transforms.
503     return renderer->localToAbsolute().y();
504 }
505
506 bool HTMLImageElement::complete() const
507 {
508     return m_imageLoader.imageComplete();
509 }
510
511 void HTMLImageElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const
512 {
513     HTMLElement::addSubresourceAttributeURLs(urls);
514
515     addSubresourceURL(urls, src());
516     // FIXME: What about when the usemap attribute begins with "#"?
517     addSubresourceURL(urls, document().completeURL(fastGetAttribute(usemapAttr)));
518 }
519
520 void HTMLImageElement::didMoveToNewDocument(Document* oldDocument)
521 {
522     m_imageLoader.elementDidMoveToNewDocument();
523     HTMLElement::didMoveToNewDocument(oldDocument);
524 }
525
526 bool HTMLImageElement::isServerMap() const
527 {
528     if (!fastHasAttribute(ismapAttr))
529         return false;
530
531     const AtomicString& usemap = fastGetAttribute(usemapAttr);
532
533     // If the usemap attribute starts with '#', it refers to a map element in the document.
534     if (usemap.string()[0] == '#')
535         return false;
536
537     return document().completeURL(stripLeadingAndTrailingHTMLSpaces(usemap)).isEmpty();
538 }
539
540 #if ENABLE(SERVICE_CONTROLS)
541 void HTMLImageElement::updateImageControls()
542 {
543     // If this image element is inside a shadow tree then it is part of an image control.
544     if (isInShadowTree())
545         return;
546
547     Settings* settings = document().settings();
548     if (!settings || !settings->imageControlsEnabled())
549         return;
550
551     bool hasControls = hasImageControls();
552     if (!m_experimentalImageMenuEnabled && hasControls)
553         destroyImageControls();
554     else if (m_experimentalImageMenuEnabled && !hasControls)
555         createImageControls();
556 }
557
558 void HTMLImageElement::createImageControls()
559 {
560     ASSERT(m_experimentalImageMenuEnabled);
561     ASSERT(!hasImageControls());
562
563     RefPtr<ImageControlsRootElement> imageControls = ImageControlsRootElement::maybeCreate(document());
564     if (!imageControls)
565         return;
566
567     ensureUserAgentShadowRoot().appendChild(imageControls.releaseNonNull());
568
569     auto* renderObject = renderer();
570     if (!renderObject)
571         return;
572
573     downcast<RenderImage>(*renderObject).setHasShadowControls(true);
574 }
575
576 void HTMLImageElement::destroyImageControls()
577 {
578     ShadowRoot* shadowRoot = userAgentShadowRoot();
579     if (!shadowRoot)
580         return;
581
582     if (Node* node = shadowRoot->firstChild()) {
583         ASSERT_WITH_SECURITY_IMPLICATION(node->isImageControlsRootElement());
584         shadowRoot->removeChild(*node);
585     }
586
587     auto* renderObject = renderer();
588     if (!renderObject)
589         return;
590
591     downcast<RenderImage>(*renderObject).setHasShadowControls(false);
592 }
593
594 bool HTMLImageElement::hasImageControls() const
595 {
596     if (ShadowRoot* shadowRoot = userAgentShadowRoot()) {
597         Node* node = shadowRoot->firstChild();
598         ASSERT_WITH_SECURITY_IMPLICATION(!node || node->isImageControlsRootElement());
599         return node;
600     }
601
602     return false;
603 }
604
605 bool HTMLImageElement::childShouldCreateRenderer(const Node& child) const
606 {
607     return hasShadowRootParent(child) && HTMLElement::childShouldCreateRenderer(child);
608 }
609 #endif // ENABLE(SERVICE_CONTROLS)
610
611 #if PLATFORM(IOS)
612 // FIXME: This is a workaround for <rdar://problem/7725158>. We should find a better place for the touchCalloutEnabled() logic.
613 bool HTMLImageElement::willRespondToMouseClickEvents()
614 {
615     auto renderer = this->renderer();
616     if (!renderer || renderer->style().touchCalloutEnabled())
617         return true;
618     return HTMLElement::willRespondToMouseClickEvents();
619 }
620 #endif
621
622 }