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.
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.
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.
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.
24 #include "HTMLImageElement.h"
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"
42 #include "RenderImage.h"
44 #include "ShadowRoot.h"
45 #include "SourceSizeList.h"
46 #include <wtf/text/StringBuilder.h>
48 #if ENABLE(SERVICE_CONTROLS)
49 #include "ImageControlsRootElement.h"
54 using namespace HTMLNames;
56 typedef HashMap<const Node*, Node*> PictureOwnerMap;
57 static PictureOwnerMap* gPictureOwnerMap = nullptr;
59 HTMLImageElement::HTMLImageElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
60 : HTMLElement(tagName, document)
61 , m_imageLoader(*this)
63 , m_formSetByParser(form)
64 , m_compositeOperator(CompositeSourceOver)
65 , m_imageDevicePixelRatio(1.0f)
66 #if ENABLE(SERVICE_CONTROLS)
67 , m_experimentalImageMenuEnabled(false)
70 ASSERT(hasTagName(imgTag));
71 setHasCustomStyleResolveCallbacks();
74 Ref<HTMLImageElement> HTMLImageElement::create(Document& document)
76 return adoptRef(*new HTMLImageElement(imgTag, document));
79 Ref<HTMLImageElement> HTMLImageElement::create(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
81 return adoptRef(*new HTMLImageElement(tagName, document, form));
84 HTMLImageElement::~HTMLImageElement()
87 m_form->removeImgElement(this);
88 setPictureNode(nullptr);
91 Ref<HTMLImageElement> HTMLImageElement::createForJSConstructor(Document& document, const int* optionalWidth, const int* optionalHeight)
93 Ref<HTMLImageElement> image = adoptRef(*new HTMLImageElement(imgTag, document));
95 image->setWidth(*optionalWidth);
97 image->setHeight(*optionalHeight);
101 bool HTMLImageElement::isPresentationAttribute(const QualifiedName& name) const
103 if (name == widthAttr || name == heightAttr || name == borderAttr || name == vspaceAttr || name == hspaceAttr || name == alignAttr || name == valignAttr)
105 return HTMLElement::isPresentationAttribute(name);
108 void HTMLImageElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style)
110 if (name == widthAttr)
111 addHTMLLengthToStyle(style, CSSPropertyWidth, value);
112 else if (name == heightAttr)
113 addHTMLLengthToStyle(style, CSSPropertyHeight, value);
114 else if (name == borderAttr)
115 applyBorderAttributeToStyle(value, style);
116 else if (name == vspaceAttr) {
117 addHTMLLengthToStyle(style, CSSPropertyMarginTop, value);
118 addHTMLLengthToStyle(style, CSSPropertyMarginBottom, value);
119 } else if (name == hspaceAttr) {
120 addHTMLLengthToStyle(style, CSSPropertyMarginLeft, value);
121 addHTMLLengthToStyle(style, CSSPropertyMarginRight, value);
122 } else if (name == alignAttr)
123 applyAlignmentAttributeToStyle(value, style);
124 else if (name == valignAttr)
125 addPropertyToPresentationAttributeStyle(style, CSSPropertyVerticalAlign, value);
127 HTMLElement::collectStyleForPresentationAttribute(name, value, style);
130 const AtomicString& HTMLImageElement::imageSourceURL() const
132 return m_bestFitImageURL.isEmpty() ? fastGetAttribute(srcAttr) : m_bestFitImageURL;
135 void HTMLImageElement::setBestFitURLAndDPRFromImageCandidate(const ImageCandidate& candidate)
137 m_bestFitImageURL = candidate.string.toString();
138 m_currentSrc = AtomicString(document().completeURL(imageSourceURL()).string());
139 if (candidate.density >= 0)
140 m_imageDevicePixelRatio = 1 / candidate.density;
141 if (is<RenderImage>(renderer()))
142 downcast<RenderImage>(*renderer()).setImageDevicePixelRatio(m_imageDevicePixelRatio);
145 ImageCandidate HTMLImageElement::bestFitSourceFromPictureElement()
147 auto* pictureOwner = pictureNode();
150 auto* picture = downcast<HTMLPictureElement>(pictureOwner);
151 picture->clearViewportDependentResults();
152 document().removeViewportDependentPicture(*picture);
153 for (Node* child = picture->firstChild(); child && child != this; child = child->nextSibling()) {
154 if (!is<HTMLSourceElement>(*child))
156 auto& source = downcast<HTMLSourceElement>(*child);
157 auto& srcset = source.fastGetAttribute(srcsetAttr);
158 if (srcset.isEmpty())
160 if (source.hasAttribute(typeAttr)) {
161 String type = source.fastGetAttribute(typeAttr).string();
162 int indexOfSemicolon = type.find(';');
163 if (indexOfSemicolon >= 0)
164 type.truncate(indexOfSemicolon);
165 type = stripLeadingAndTrailingHTMLSpaces(type);
167 if (!type.isEmpty() && !MIMETypeRegistry::isSupportedImageMIMEType(type) && type != "image/svg+xml")
170 MediaQueryEvaluator evaluator(document().printing() ? "print" : "screen", document().frame(), document().documentElement()->computedStyle());
171 bool evaluation = evaluator.evalCheckingViewportDependentResults(source.mediaQuerySet(), picture->viewportDependentResults());
172 if (picture->hasViewportDependentResults())
173 document().addViewportDependentPicture(*picture);
177 float sourceSize = parseSizesAttribute(source.fastGetAttribute(sizesAttr).string(), document().renderView(), document().frame());
178 ImageCandidate candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), nullAtom, source.fastGetAttribute(srcsetAttr), sourceSize);
179 if (!candidate.isEmpty())
185 void HTMLImageElement::selectImageSource()
187 // First look for the best fit source from our <picture> parent if we have one.
188 ImageCandidate candidate = bestFitSourceFromPictureElement();
189 if (candidate.isEmpty()) {
190 // If we don't have a <picture> or didn't find a source, then we use our own attributes.
191 float sourceSize = parseSizesAttribute(fastGetAttribute(sizesAttr).string(), document().renderView(), document().frame());
192 candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), fastGetAttribute(srcAttr), fastGetAttribute(srcsetAttr), sourceSize);
194 setBestFitURLAndDPRFromImageCandidate(candidate);
195 m_imageLoader.updateFromElementIgnoringPreviousError();
198 void HTMLImageElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
200 if (name == altAttr) {
201 if (is<RenderImage>(renderer()))
202 downcast<RenderImage>(*renderer()).updateAltText();
203 } else if (name == srcAttr || name == srcsetAttr || name == sizesAttr)
205 else if (name == usemapAttr) {
206 if (inDocument() && !m_lowercasedUsemap.isNull())
207 document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
209 // The HTMLImageElement's useMap() value includes the '#' symbol at the beginning, which has to be stripped off.
210 // FIXME: We should check that the first character is '#'.
211 // FIXME: HTML5 specification says we should strip any leading string before '#'.
212 // FIXME: HTML5 specification says we should ignore usemap attributes without #.
213 if (value.length() > 1)
214 m_lowercasedUsemap = value.string().substring(1).lower();
216 m_lowercasedUsemap = nullAtom;
218 if (inDocument() && !m_lowercasedUsemap.isNull())
219 document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
220 } else if (name == compositeAttr) {
221 // FIXME: images don't support blend modes in their compositing attribute.
222 BlendMode blendOp = BlendModeNormal;
223 if (!parseCompositeAndBlendOperator(value, m_compositeOperator, blendOp))
224 m_compositeOperator = CompositeSourceOver;
225 #if ENABLE(SERVICE_CONTROLS)
226 } else if (name == webkitimagemenuAttr) {
227 m_experimentalImageMenuEnabled = !value.isNull();
228 updateImageControls();
231 if (name == nameAttr) {
232 bool willHaveName = !value.isNull();
233 if (m_hadNameBeforeAttributeChanged != willHaveName && inDocument() && is<HTMLDocument>(document())) {
234 HTMLDocument& document = downcast<HTMLDocument>(this->document());
235 const AtomicString& id = getIdAttribute();
236 if (!id.isEmpty() && id != getNameAttribute()) {
238 document.addDocumentNamedItem(*id.impl(), *this);
240 document.removeDocumentNamedItem(*id.impl(), *this);
243 m_hadNameBeforeAttributeChanged = willHaveName;
245 HTMLElement::parseAttribute(name, value);
249 const AtomicString& HTMLImageElement::altText() const
251 // lets figure out the alt text.. magic stuff
252 // http://www.w3.org/TR/1998/REC-html40-19980424/appendix/notes.html#altgen
253 // also heavily discussed by Hixie on bugzilla
254 const AtomicString& alt = fastGetAttribute(altAttr);
257 // fall back to title attribute
258 return fastGetAttribute(titleAttr);
261 RenderPtr<RenderElement> HTMLImageElement::createElementRenderer(Ref<RenderStyle>&& style, const RenderTreePosition&)
263 if (style.get().hasContent())
264 return RenderElement::createFor(*this, WTFMove(style));
266 return createRenderer<RenderImage>(*this, WTFMove(style), nullptr, m_imageDevicePixelRatio);
269 bool HTMLImageElement::canStartSelection() const
272 return HTMLElement::canStartSelection();
277 void HTMLImageElement::didAttachRenderers()
279 if (!is<RenderImage>(renderer()))
281 if (m_imageLoader.hasPendingBeforeLoadEvent())
284 #if ENABLE(SERVICE_CONTROLS)
285 updateImageControls();
288 auto& renderImage = downcast<RenderImage>(*renderer());
289 RenderImageResource& renderImageResource = renderImage.imageResource();
290 if (renderImageResource.hasImage())
292 renderImageResource.setCachedImage(m_imageLoader.image());
294 // If we have no image at all because we have no src attribute, set
295 // image height and width for the alt text instead.
296 if (!m_imageLoader.image() && !renderImageResource.cachedImage())
297 renderImage.setImageSizeForAltText();
300 Node::InsertionNotificationRequest HTMLImageElement::insertedInto(ContainerNode& insertionPoint)
302 if (m_formSetByParser) {
303 m_form = m_formSetByParser;
304 m_formSetByParser = nullptr;
305 m_form->registerImgElement(this);
309 m_form = HTMLFormElement::findClosestFormAncestor(*this);
311 m_form->registerImgElement(this);
313 // Insert needs to complete first, before we start updating the loader. Loader dispatches events which could result
314 // in callbacks back to this node.
315 Node::InsertionNotificationRequest insertNotificationRequest = HTMLElement::insertedInto(insertionPoint);
317 if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
318 document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
320 if (is<HTMLPictureElement>(parentNode())) {
321 setPictureNode(parentNode());
325 // If we have been inserted from a renderer-less document,
326 // our loader may have not fetched the image, so do it now.
327 if (insertionPoint.inDocument() && !m_imageLoader.image())
328 m_imageLoader.updateFromElement();
330 return insertNotificationRequest;
333 void HTMLImageElement::removedFrom(ContainerNode& insertionPoint)
336 m_form->removeImgElement(this);
338 if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
339 document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
341 if (is<HTMLPictureElement>(parentNode()))
342 setPictureNode(nullptr);
345 HTMLElement::removedFrom(insertionPoint);
348 Node* HTMLImageElement::pictureNode() const
350 if (!gPictureOwnerMap)
352 return gPictureOwnerMap->get(this);
355 void HTMLImageElement::setPictureNode(Node* node)
358 if (gPictureOwnerMap)
359 gPictureOwnerMap->remove(this);
363 if (!gPictureOwnerMap)
364 gPictureOwnerMap = new PictureOwnerMap();
365 gPictureOwnerMap->add(this, node);
368 int HTMLImageElement::width(bool ignorePendingStylesheets)
371 // check the attribute first for an explicit pixel value
373 int width = getAttribute(widthAttr).toInt(&ok);
377 // if the image is available, use its width
378 if (m_imageLoader.image())
379 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
382 if (ignorePendingStylesheets)
383 document().updateLayoutIgnorePendingStylesheets();
385 document().updateLayout();
387 RenderBox* box = renderBox();
390 LayoutRect contentRect = box->contentBoxRect();
391 return adjustForAbsoluteZoom(snappedIntRect(contentRect).width(), *box);
394 int HTMLImageElement::height(bool ignorePendingStylesheets)
397 // check the attribute first for an explicit pixel value
399 int height = getAttribute(heightAttr).toInt(&ok);
403 // if the image is available, use its height
404 if (m_imageLoader.image())
405 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
408 if (ignorePendingStylesheets)
409 document().updateLayoutIgnorePendingStylesheets();
411 document().updateLayout();
413 RenderBox* box = renderBox();
416 LayoutRect contentRect = box->contentBoxRect();
417 return adjustForAbsoluteZoom(snappedIntRect(contentRect).height(), *box);
420 int HTMLImageElement::naturalWidth() const
422 if (!m_imageLoader.image())
425 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
428 int HTMLImageElement::naturalHeight() const
430 if (!m_imageLoader.image())
433 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
436 bool HTMLImageElement::isURLAttribute(const Attribute& attribute) const
438 return attribute.name() == srcAttr
439 || attribute.name() == lowsrcAttr
440 || attribute.name() == longdescAttr
441 || (attribute.name() == usemapAttr && attribute.value().string()[0] != '#')
442 || HTMLElement::isURLAttribute(attribute);
445 bool HTMLImageElement::attributeContainsURL(const Attribute& attribute) const
447 return attribute.name() == srcsetAttr
448 || HTMLElement::attributeContainsURL(attribute);
451 String HTMLImageElement::completeURLsInAttributeValue(const URL& base, const Attribute& attribute) const
453 if (attribute.name() == srcsetAttr) {
454 Vector<ImageCandidate> imageCandidates = parseImageCandidatesFromSrcsetAttribute(StringView(attribute.value()));
455 StringBuilder result;
456 for (const auto& candidate : imageCandidates) {
457 if (&candidate != &imageCandidates[0])
458 result.appendLiteral(", ");
459 result.append(URL(base, candidate.string.toString()).string());
460 if (candidate.density != UninitializedDescriptor) {
462 result.appendNumber(candidate.density);
465 if (candidate.resourceWidth != UninitializedDescriptor) {
467 result.appendNumber(candidate.resourceWidth);
471 return result.toString();
473 return HTMLElement::completeURLsInAttributeValue(base, attribute);
476 bool HTMLImageElement::matchesLowercasedUsemap(const AtomicStringImpl& name) const
478 ASSERT(String(&const_cast<AtomicStringImpl&>(name)).lower().impl() == &name);
479 return m_lowercasedUsemap.impl() == &name;
482 const AtomicString& HTMLImageElement::alt() const
484 return fastGetAttribute(altAttr);
487 bool HTMLImageElement::draggable() const
489 // Image elements are draggable by default.
490 return !equalIgnoringCase(fastGetAttribute(draggableAttr), "false");
493 void HTMLImageElement::setHeight(int value)
495 setIntegralAttribute(heightAttr, value);
498 URL HTMLImageElement::src() const
500 return document().completeURL(fastGetAttribute(srcAttr));
503 void HTMLImageElement::setSrc(const String& value)
505 setAttribute(srcAttr, value);
508 void HTMLImageElement::setWidth(int value)
510 setIntegralAttribute(widthAttr, value);
513 int HTMLImageElement::x() const
515 document().updateLayoutIgnorePendingStylesheets();
516 auto renderer = this->renderer();
520 // FIXME: This doesn't work correctly with transforms.
521 return renderer->localToAbsolute().x();
524 int HTMLImageElement::y() const
526 document().updateLayoutIgnorePendingStylesheets();
527 auto renderer = this->renderer();
531 // FIXME: This doesn't work correctly with transforms.
532 return renderer->localToAbsolute().y();
535 bool HTMLImageElement::complete() const
537 return m_imageLoader.imageComplete();
540 void HTMLImageElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const
542 HTMLElement::addSubresourceAttributeURLs(urls);
544 addSubresourceURL(urls, src());
545 // FIXME: What about when the usemap attribute begins with "#"?
546 addSubresourceURL(urls, document().completeURL(fastGetAttribute(usemapAttr)));
549 void HTMLImageElement::didMoveToNewDocument(Document* oldDocument)
551 m_imageLoader.elementDidMoveToNewDocument();
552 HTMLElement::didMoveToNewDocument(oldDocument);
555 bool HTMLImageElement::isServerMap() const
557 if (!fastHasAttribute(ismapAttr))
560 const AtomicString& usemap = fastGetAttribute(usemapAttr);
562 // If the usemap attribute starts with '#', it refers to a map element in the document.
563 if (usemap.string()[0] == '#')
566 return document().completeURL(stripLeadingAndTrailingHTMLSpaces(usemap)).isEmpty();
569 #if ENABLE(SERVICE_CONTROLS)
570 void HTMLImageElement::updateImageControls()
572 // If this image element is inside a shadow tree then it is part of an image control.
573 if (isInShadowTree())
576 Settings* settings = document().settings();
577 if (!settings || !settings->imageControlsEnabled())
580 bool hasControls = hasImageControls();
581 if (!m_experimentalImageMenuEnabled && hasControls)
582 destroyImageControls();
583 else if (m_experimentalImageMenuEnabled && !hasControls)
584 createImageControls();
587 void HTMLImageElement::createImageControls()
589 ASSERT(m_experimentalImageMenuEnabled);
590 ASSERT(!hasImageControls());
592 RefPtr<ImageControlsRootElement> imageControls = ImageControlsRootElement::maybeCreate(document());
596 ensureUserAgentShadowRoot().appendChild(imageControls.releaseNonNull());
598 auto* renderObject = renderer();
602 downcast<RenderImage>(*renderObject).setHasShadowControls(true);
605 void HTMLImageElement::destroyImageControls()
607 ShadowRoot* shadowRoot = userAgentShadowRoot();
611 if (Node* node = shadowRoot->firstChild()) {
612 ASSERT_WITH_SECURITY_IMPLICATION(node->isImageControlsRootElement());
613 shadowRoot->removeChild(*node);
616 auto* renderObject = renderer();
620 downcast<RenderImage>(*renderObject).setHasShadowControls(false);
623 bool HTMLImageElement::hasImageControls() const
625 if (ShadowRoot* shadowRoot = userAgentShadowRoot()) {
626 Node* node = shadowRoot->firstChild();
627 ASSERT_WITH_SECURITY_IMPLICATION(!node || node->isImageControlsRootElement());
634 bool HTMLImageElement::childShouldCreateRenderer(const Node& child) const
636 return hasShadowRootParent(child) && HTMLElement::childShouldCreateRenderer(child);
638 #endif // ENABLE(SERVICE_CONTROLS)
641 // FIXME: This is a workaround for <rdar://problem/7725158>. We should find a better place for the touchCalloutEnabled() logic.
642 bool HTMLImageElement::willRespondToMouseClickEvents()
644 auto renderer = this->renderer();
645 if (!renderer || renderer->style().touchCalloutEnabled())
647 return HTMLElement::willRespondToMouseClickEvents();