Implement the picture element.
[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 "HTMLSourceElement.h"
36 #include "HTMLSrcsetParser.h"
37 #include "MIMETypeRegistry.h"
38 #include "MediaList.h"
39 #include "MediaQueryEvaluator.h"
40 #include "Page.h"
41 #include "RenderImage.h"
42 #include "Settings.h"
43 #include "ShadowRoot.h"
44 #include "SourceSizeList.h"
45 #include <wtf/text/StringBuilder.h>
46
47 #if ENABLE(SERVICE_CONTROLS)
48 #include "ImageControlsRootElement.h"
49 #endif
50
51 namespace WebCore {
52
53 using namespace HTMLNames;
54
55 HTMLImageElement::HTMLImageElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
56     : HTMLElement(tagName, document)
57     , m_imageLoader(*this)
58     , m_form(form)
59     , m_compositeOperator(CompositeSourceOver)
60     , m_imageDevicePixelRatio(1.0f)
61 #if ENABLE(SERVICE_CONTROLS)
62     , m_experimentalImageMenuEnabled(false)
63 #endif
64 {
65     ASSERT(hasTagName(imgTag));
66     setHasCustomStyleResolveCallbacks();
67     if (form)
68         form->registerImgElement(this);
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     for (Node* child = parent->firstChild(); child && child != this; child = child->nextSibling()) {
147         if (!is<HTMLSourceElement>(*child))
148             continue;
149         auto& source = downcast<HTMLSourceElement>(*child);
150         auto& srcset = source.fastGetAttribute(srcsetAttr);
151         if (srcset.isEmpty())
152             continue;
153         if (source.hasAttribute(typeAttr)) {
154             String type = source.fastGetAttribute(typeAttr).string();
155             int indexOfSemicolon = type.find(';');
156             if (indexOfSemicolon >= 0)
157                 type.truncate(indexOfSemicolon);
158             type = stripLeadingAndTrailingHTMLSpaces(type);
159             type = type.lower();
160             if (!type.isEmpty() && !MIMETypeRegistry::isSupportedImageMIMEType(type) && type != "image/svg+xml")
161                 continue;
162         }
163         MediaQueryEvaluator evaluator(document().printing() ? "print" : "screen", document().frame(), computedStyle());
164         if (!evaluator.eval(MediaQuerySet::createAllowingDescriptionSyntax(source.media()).ptr()))
165             continue;
166         
167         float sourceSize = parseSizesAttribute(source.fastGetAttribute(sizesAttr).string(), document().renderView(), document().frame());
168         ImageCandidate candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), nullAtom, source.fastGetAttribute(srcsetAttr), sourceSize);
169         if (!candidate.isEmpty())
170             return candidate;
171     }
172     return { };
173 }
174
175 void HTMLImageElement::selectImageSource()
176 {
177     // First look for the best fit source from our <picture> parent if we have one.
178     ImageCandidate candidate = bestFitSourceFromPictureElement();
179     if (candidate.isEmpty()) {
180         // If we don't have a <picture> or didn't find a source, then we use our own attributes.
181         float sourceSize = parseSizesAttribute(fastGetAttribute(sizesAttr).string(), document().renderView(), document().frame());
182         candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), fastGetAttribute(srcAttr), fastGetAttribute(srcsetAttr), sourceSize);
183     }
184     setBestFitURLAndDPRFromImageCandidate(candidate);
185     m_imageLoader.updateFromElementIgnoringPreviousError();
186 }
187
188 void HTMLImageElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
189 {
190     if (name == altAttr) {
191         if (is<RenderImage>(renderer()))
192             downcast<RenderImage>(*renderer()).updateAltText();
193     } else if (name == srcAttr || name == srcsetAttr || name == sizesAttr)
194         selectImageSource();
195     else if (name == usemapAttr) {
196         if (inDocument() && !m_lowercasedUsemap.isNull())
197             document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
198
199         // The HTMLImageElement's useMap() value includes the '#' symbol at the beginning, which has to be stripped off.
200         // FIXME: We should check that the first character is '#'.
201         // FIXME: HTML5 specification says we should strip any leading string before '#'.
202         // FIXME: HTML5 specification says we should ignore usemap attributes without #.
203         if (value.length() > 1)
204             m_lowercasedUsemap = value.string().substring(1).lower();
205         else
206             m_lowercasedUsemap = nullAtom;
207
208         if (inDocument() && !m_lowercasedUsemap.isNull())
209             document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
210     } else if (name == compositeAttr) {
211         // FIXME: images don't support blend modes in their compositing attribute.
212         BlendMode blendOp = BlendModeNormal;
213         if (!parseCompositeAndBlendOperator(value, m_compositeOperator, blendOp))
214             m_compositeOperator = CompositeSourceOver;
215 #if ENABLE(SERVICE_CONTROLS)
216     } else if (name == webkitimagemenuAttr) {
217         m_experimentalImageMenuEnabled = !value.isNull();
218         updateImageControls();
219 #endif
220     } else {
221         if (name == nameAttr) {
222             bool willHaveName = !value.isNull();
223             if (m_hadNameBeforeAttributeChanged != willHaveName && inDocument() && is<HTMLDocument>(document())) {
224                 HTMLDocument& document = downcast<HTMLDocument>(this->document());
225                 const AtomicString& id = getIdAttribute();
226                 if (!id.isEmpty() && id != getNameAttribute()) {
227                     if (willHaveName)
228                         document.addDocumentNamedItem(*id.impl(), *this);
229                     else
230                         document.removeDocumentNamedItem(*id.impl(), *this);
231                 }
232             }
233             m_hadNameBeforeAttributeChanged = willHaveName;
234         }
235         HTMLElement::parseAttribute(name, value);
236     }
237 }
238
239 const AtomicString& HTMLImageElement::altText() const
240 {
241     // lets figure out the alt text.. magic stuff
242     // http://www.w3.org/TR/1998/REC-html40-19980424/appendix/notes.html#altgen
243     // also heavily discussed by Hixie on bugzilla
244     const AtomicString& alt = fastGetAttribute(altAttr);
245     if (!alt.isNull())
246         return alt;
247     // fall back to title attribute
248     return fastGetAttribute(titleAttr);
249 }
250
251 RenderPtr<RenderElement> HTMLImageElement::createElementRenderer(Ref<RenderStyle>&& style, const RenderTreePosition&)
252 {
253     if (style.get().hasContent())
254         return RenderElement::createFor(*this, WTF::move(style));
255
256     return createRenderer<RenderImage>(*this, WTF::move(style), nullptr, m_imageDevicePixelRatio);
257 }
258
259 bool HTMLImageElement::canStartSelection() const
260 {
261     if (shadowRoot())
262         return HTMLElement::canStartSelection();
263
264     return false;
265 }
266
267 void HTMLImageElement::didAttachRenderers()
268 {
269     if (!is<RenderImage>(renderer()))
270         return;
271     if (m_imageLoader.hasPendingBeforeLoadEvent())
272         return;
273
274 #if ENABLE(SERVICE_CONTROLS)
275     updateImageControls();
276 #endif
277
278     auto& renderImage = downcast<RenderImage>(*renderer());
279     RenderImageResource& renderImageResource = renderImage.imageResource();
280     if (renderImageResource.hasImage())
281         return;
282     renderImageResource.setCachedImage(m_imageLoader.image());
283
284     // If we have no image at all because we have no src attribute, set
285     // image height and width for the alt text instead.
286     if (!m_imageLoader.image() && !renderImageResource.cachedImage())
287         renderImage.setImageSizeForAltText();
288 }
289
290 Node::InsertionNotificationRequest HTMLImageElement::insertedInto(ContainerNode& insertionPoint)
291 {
292     if (!m_form) { // m_form can be non-null if it was set in constructor.
293         m_form = HTMLFormElement::findClosestFormAncestor(*this);
294         if (m_form)
295             m_form->registerImgElement(this);
296     }
297
298     // Insert needs to complete first, before we start updating the loader. Loader dispatches events which could result
299     // in callbacks back to this node.
300     Node::InsertionNotificationRequest insertNotificationRequest = HTMLElement::insertedInto(insertionPoint);
301
302     if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
303         document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
304     
305     if (is<HTMLPictureElement>(parentNode()))
306         selectImageSource();
307     
308     // If we have been inserted from a renderer-less document,
309     // our loader may have not fetched the image, so do it now.
310     if (insertionPoint.inDocument() && !m_imageLoader.image())
311         m_imageLoader.updateFromElement();
312
313     return insertNotificationRequest;
314 }
315
316 void HTMLImageElement::removedFrom(ContainerNode& insertionPoint)
317 {
318     if (m_form)
319         m_form->removeImgElement(this);
320
321     if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
322         document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
323
324     m_form = 0;
325     HTMLElement::removedFrom(insertionPoint);
326 }
327
328 int HTMLImageElement::width(bool ignorePendingStylesheets)
329 {
330     if (!renderer()) {
331         // check the attribute first for an explicit pixel value
332         bool ok;
333         int width = getAttribute(widthAttr).toInt(&ok);
334         if (ok)
335             return width;
336
337         // if the image is available, use its width
338         if (m_imageLoader.image())
339             return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
340     }
341
342     if (ignorePendingStylesheets)
343         document().updateLayoutIgnorePendingStylesheets();
344     else
345         document().updateLayout();
346
347     RenderBox* box = renderBox();
348     if (!box)
349         return 0;
350     LayoutRect contentRect = box->contentBoxRect();
351     return adjustForAbsoluteZoom(snappedIntRect(contentRect).width(), *box);
352 }
353
354 int HTMLImageElement::height(bool ignorePendingStylesheets)
355 {
356     if (!renderer()) {
357         // check the attribute first for an explicit pixel value
358         bool ok;
359         int height = getAttribute(heightAttr).toInt(&ok);
360         if (ok)
361             return height;
362
363         // if the image is available, use its height
364         if (m_imageLoader.image())
365             return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
366     }
367
368     if (ignorePendingStylesheets)
369         document().updateLayoutIgnorePendingStylesheets();
370     else
371         document().updateLayout();
372
373     RenderBox* box = renderBox();
374     if (!box)
375         return 0;
376     LayoutRect contentRect = box->contentBoxRect();
377     return adjustForAbsoluteZoom(snappedIntRect(contentRect).height(), *box);
378 }
379
380 int HTMLImageElement::naturalWidth() const
381 {
382     if (!m_imageLoader.image())
383         return 0;
384
385     return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
386 }
387
388 int HTMLImageElement::naturalHeight() const
389 {
390     if (!m_imageLoader.image())
391         return 0;
392
393     return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
394 }
395
396 bool HTMLImageElement::isURLAttribute(const Attribute& attribute) const
397 {
398     return attribute.name() == srcAttr
399         || attribute.name() == lowsrcAttr
400         || attribute.name() == longdescAttr
401         || (attribute.name() == usemapAttr && attribute.value().string()[0] != '#')
402         || HTMLElement::isURLAttribute(attribute);
403 }
404
405 bool HTMLImageElement::attributeContainsURL(const Attribute& attribute) const
406 {
407     return attribute.name() == srcsetAttr
408         || HTMLElement::attributeContainsURL(attribute);
409 }
410
411 String HTMLImageElement::completeURLsInAttributeValue(const URL& base, const Attribute& attribute) const
412 {
413     if (attribute.name() == srcsetAttr) {
414         Vector<ImageCandidate> imageCandidates = parseImageCandidatesFromSrcsetAttribute(StringView(attribute.value()));
415         StringBuilder result;
416         for (const auto& candidate : imageCandidates) {
417             if (&candidate != &imageCandidates[0])
418                 result.appendLiteral(", ");
419             result.append(URL(base, candidate.string.toString()).string());
420             if (candidate.density != UninitializedDescriptor) {
421                 result.append(' ');
422                 result.appendNumber(candidate.density);
423                 result.append('x');
424             }
425             if (candidate.resourceWidth != UninitializedDescriptor) {
426                 result.append(' ');
427                 result.appendNumber(candidate.resourceWidth);
428                 result.append('x');
429             }
430         }
431         return result.toString();
432     }
433     return HTMLElement::completeURLsInAttributeValue(base, attribute);
434 }
435
436 bool HTMLImageElement::matchesLowercasedUsemap(const AtomicStringImpl& name) const
437 {
438     ASSERT(String(&const_cast<AtomicStringImpl&>(name)).lower().impl() == &name);
439     return m_lowercasedUsemap.impl() == &name;
440 }
441
442 const AtomicString& HTMLImageElement::alt() const
443 {
444     return fastGetAttribute(altAttr);
445 }
446
447 bool HTMLImageElement::draggable() const
448 {
449     // Image elements are draggable by default.
450     return !equalIgnoringCase(fastGetAttribute(draggableAttr), "false");
451 }
452
453 void HTMLImageElement::setHeight(int value)
454 {
455     setIntegralAttribute(heightAttr, value);
456 }
457
458 URL HTMLImageElement::src() const
459 {
460     return document().completeURL(fastGetAttribute(srcAttr));
461 }
462
463 void HTMLImageElement::setSrc(const String& value)
464 {
465     setAttribute(srcAttr, value);
466 }
467
468 void HTMLImageElement::setWidth(int value)
469 {
470     setIntegralAttribute(widthAttr, value);
471 }
472
473 int HTMLImageElement::x() const
474 {
475     document().updateLayoutIgnorePendingStylesheets();
476     auto renderer = this->renderer();
477     if (!renderer)
478         return 0;
479
480     // FIXME: This doesn't work correctly with transforms.
481     return renderer->localToAbsolute().x();
482 }
483
484 int HTMLImageElement::y() 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().y();
493 }
494
495 bool HTMLImageElement::complete() const
496 {
497     return m_imageLoader.imageComplete();
498 }
499
500 void HTMLImageElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const
501 {
502     HTMLElement::addSubresourceAttributeURLs(urls);
503
504     addSubresourceURL(urls, src());
505     // FIXME: What about when the usemap attribute begins with "#"?
506     addSubresourceURL(urls, document().completeURL(fastGetAttribute(usemapAttr)));
507 }
508
509 void HTMLImageElement::didMoveToNewDocument(Document* oldDocument)
510 {
511     m_imageLoader.elementDidMoveToNewDocument();
512     HTMLElement::didMoveToNewDocument(oldDocument);
513 }
514
515 bool HTMLImageElement::isServerMap() const
516 {
517     if (!fastHasAttribute(ismapAttr))
518         return false;
519
520     const AtomicString& usemap = fastGetAttribute(usemapAttr);
521     
522     // If the usemap attribute starts with '#', it refers to a map element in the document.
523     if (usemap.string()[0] == '#')
524         return false;
525
526     return document().completeURL(stripLeadingAndTrailingHTMLSpaces(usemap)).isEmpty();
527 }
528
529 #if ENABLE(SERVICE_CONTROLS)
530 void HTMLImageElement::updateImageControls()
531 {
532     // If this image element is inside a shadow tree then it is part of an image control.
533     if (isInShadowTree())
534         return;
535
536     Settings* settings = document().settings();
537     if (!settings || !settings->imageControlsEnabled())
538         return;
539
540     bool hasControls = hasImageControls();
541     if (!m_experimentalImageMenuEnabled && hasControls)
542         destroyImageControls();
543     else if (m_experimentalImageMenuEnabled && !hasControls)
544         createImageControls();
545 }
546
547 void HTMLImageElement::createImageControls()
548 {
549     ASSERT(m_experimentalImageMenuEnabled);
550     ASSERT(!hasImageControls());
551
552     RefPtr<ImageControlsRootElement> imageControls = ImageControlsRootElement::maybeCreate(document());
553     if (!imageControls)
554         return;
555
556     ensureUserAgentShadowRoot().appendChild(imageControls.releaseNonNull());
557
558     auto* renderObject = renderer();
559     if (!renderObject)
560         return;
561
562     downcast<RenderImage>(*renderObject).setHasShadowControls(true);
563 }
564
565 void HTMLImageElement::destroyImageControls()
566 {
567     ShadowRoot* shadowRoot = userAgentShadowRoot();
568     if (!shadowRoot)
569         return;
570
571     if (Node* node = shadowRoot->firstChild()) {
572         ASSERT_WITH_SECURITY_IMPLICATION(node->isImageControlsRootElement());
573         shadowRoot->removeChild(*node);
574     }
575
576     auto* renderObject = renderer();
577     if (!renderObject)
578         return;
579
580     downcast<RenderImage>(*renderObject).setHasShadowControls(false);
581 }
582
583 bool HTMLImageElement::hasImageControls() const
584 {
585     if (ShadowRoot* shadowRoot = userAgentShadowRoot()) {
586         Node* node = shadowRoot->firstChild();
587         ASSERT_WITH_SECURITY_IMPLICATION(!node || node->isImageControlsRootElement());
588         return node;
589     }
590
591     return false;
592 }
593
594 bool HTMLImageElement::childShouldCreateRenderer(const Node& child) const
595 {
596     return hasShadowRootParent(child) && HTMLElement::childShouldCreateRenderer(child);
597 }
598 #endif // ENABLE(SERVICE_CONTROLS)
599
600 #if PLATFORM(IOS)
601 // FIXME: This is a workaround for <rdar://problem/7725158>. We should find a better place for the touchCalloutEnabled() logic.
602 bool HTMLImageElement::willRespondToMouseClickEvents()
603 {
604     auto renderer = this->renderer();
605     if (!renderer || renderer->style().touchCalloutEnabled())
606         return true;
607     return HTMLElement::willRespondToMouseClickEvents();
608 }
609 #endif
610
611 }