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 HTMLImageElement::HTMLImageElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
57 : HTMLElement(tagName, document)
58 , m_imageLoader(*this)
60 , m_formSetByParser(form)
61 , m_compositeOperator(CompositeSourceOver)
62 , m_imageDevicePixelRatio(1.0f)
63 #if ENABLE(SERVICE_CONTROLS)
64 , m_experimentalImageMenuEnabled(false)
67 ASSERT(hasTagName(imgTag));
68 setHasCustomStyleResolveCallbacks();
71 Ref<HTMLImageElement> HTMLImageElement::create(Document& document)
73 return adoptRef(*new HTMLImageElement(imgTag, document));
76 Ref<HTMLImageElement> HTMLImageElement::create(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
78 return adoptRef(*new HTMLImageElement(tagName, document, form));
81 HTMLImageElement::~HTMLImageElement()
84 m_form->removeImgElement(this);
87 Ref<HTMLImageElement> HTMLImageElement::createForJSConstructor(Document& document, const int* optionalWidth, const int* optionalHeight)
89 Ref<HTMLImageElement> image = adoptRef(*new HTMLImageElement(imgTag, document));
91 image->setWidth(*optionalWidth);
93 image->setHeight(*optionalHeight);
97 bool HTMLImageElement::isPresentationAttribute(const QualifiedName& name) const
99 if (name == widthAttr || name == heightAttr || name == borderAttr || name == vspaceAttr || name == hspaceAttr || name == alignAttr || name == valignAttr)
101 return HTMLElement::isPresentationAttribute(name);
104 void HTMLImageElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style)
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);
123 HTMLElement::collectStyleForPresentationAttribute(name, value, style);
126 const AtomicString& HTMLImageElement::imageSourceURL() const
128 return m_bestFitImageURL.isEmpty() ? fastGetAttribute(srcAttr) : m_bestFitImageURL;
131 void HTMLImageElement::setBestFitURLAndDPRFromImageCandidate(const ImageCandidate& candidate)
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);
141 ImageCandidate HTMLImageElement::bestFitSourceFromPictureElement()
143 auto* parent = parentNode();
144 if (!is<HTMLPictureElement>(parent))
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))
152 auto& source = downcast<HTMLSourceElement>(*child);
153 auto& srcset = source.fastGetAttribute(srcsetAttr);
154 if (srcset.isEmpty())
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);
163 if (!type.isEmpty() && !MIMETypeRegistry::isSupportedImageMIMEType(type) && type != "image/svg+xml")
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);
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())
181 void HTMLImageElement::selectImageSource()
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);
190 setBestFitURLAndDPRFromImageCandidate(candidate);
191 m_imageLoader.updateFromElementIgnoringPreviousError();
194 void HTMLImageElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
196 if (name == altAttr) {
197 if (is<RenderImage>(renderer()))
198 downcast<RenderImage>(*renderer()).updateAltText();
199 } else if (name == srcAttr || name == srcsetAttr || name == sizesAttr)
201 else if (name == usemapAttr) {
202 if (inDocument() && !m_lowercasedUsemap.isNull())
203 document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
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();
212 m_lowercasedUsemap = nullAtom;
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();
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()) {
234 document.addDocumentNamedItem(*id.impl(), *this);
236 document.removeDocumentNamedItem(*id.impl(), *this);
239 m_hadNameBeforeAttributeChanged = willHaveName;
241 HTMLElement::parseAttribute(name, value);
245 const AtomicString& HTMLImageElement::altText() const
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);
253 // fall back to title attribute
254 return fastGetAttribute(titleAttr);
257 RenderPtr<RenderElement> HTMLImageElement::createElementRenderer(Ref<RenderStyle>&& style, const RenderTreePosition&)
259 if (style.get().hasContent())
260 return RenderElement::createFor(*this, WTFMove(style));
262 return createRenderer<RenderImage>(*this, WTFMove(style), nullptr, m_imageDevicePixelRatio);
265 bool HTMLImageElement::canStartSelection() const
268 return HTMLElement::canStartSelection();
273 void HTMLImageElement::didAttachRenderers()
275 if (!is<RenderImage>(renderer()))
277 if (m_imageLoader.hasPendingBeforeLoadEvent())
280 #if ENABLE(SERVICE_CONTROLS)
281 updateImageControls();
284 auto& renderImage = downcast<RenderImage>(*renderer());
285 RenderImageResource& renderImageResource = renderImage.imageResource();
286 if (renderImageResource.hasImage())
288 renderImageResource.setCachedImage(m_imageLoader.image());
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();
296 Node::InsertionNotificationRequest HTMLImageElement::insertedInto(ContainerNode& insertionPoint)
298 if (m_formSetByParser) {
299 m_form = m_formSetByParser;
300 m_formSetByParser = nullptr;
301 m_form->registerImgElement(this);
305 m_form = HTMLFormElement::findClosestFormAncestor(*this);
307 m_form->registerImgElement(this);
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);
313 if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
314 document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
316 if (is<HTMLPictureElement>(parentNode()))
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();
324 return insertNotificationRequest;
327 void HTMLImageElement::removedFrom(ContainerNode& insertionPoint)
330 m_form->removeImgElement(this);
332 if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
333 document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
336 HTMLElement::removedFrom(insertionPoint);
339 int HTMLImageElement::width(bool ignorePendingStylesheets)
342 // check the attribute first for an explicit pixel value
344 int width = getAttribute(widthAttr).toInt(&ok);
348 // if the image is available, use its width
349 if (m_imageLoader.image())
350 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
353 if (ignorePendingStylesheets)
354 document().updateLayoutIgnorePendingStylesheets();
356 document().updateLayout();
358 RenderBox* box = renderBox();
361 LayoutRect contentRect = box->contentBoxRect();
362 return adjustForAbsoluteZoom(snappedIntRect(contentRect).width(), *box);
365 int HTMLImageElement::height(bool ignorePendingStylesheets)
368 // check the attribute first for an explicit pixel value
370 int height = getAttribute(heightAttr).toInt(&ok);
374 // if the image is available, use its height
375 if (m_imageLoader.image())
376 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
379 if (ignorePendingStylesheets)
380 document().updateLayoutIgnorePendingStylesheets();
382 document().updateLayout();
384 RenderBox* box = renderBox();
387 LayoutRect contentRect = box->contentBoxRect();
388 return adjustForAbsoluteZoom(snappedIntRect(contentRect).height(), *box);
391 int HTMLImageElement::naturalWidth() const
393 if (!m_imageLoader.image())
396 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
399 int HTMLImageElement::naturalHeight() const
401 if (!m_imageLoader.image())
404 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
407 bool HTMLImageElement::isURLAttribute(const Attribute& attribute) const
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);
416 bool HTMLImageElement::attributeContainsURL(const Attribute& attribute) const
418 return attribute.name() == srcsetAttr
419 || HTMLElement::attributeContainsURL(attribute);
422 String HTMLImageElement::completeURLsInAttributeValue(const URL& base, const Attribute& attribute) const
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) {
433 result.appendNumber(candidate.density);
436 if (candidate.resourceWidth != UninitializedDescriptor) {
438 result.appendNumber(candidate.resourceWidth);
442 return result.toString();
444 return HTMLElement::completeURLsInAttributeValue(base, attribute);
447 bool HTMLImageElement::matchesLowercasedUsemap(const AtomicStringImpl& name) const
449 ASSERT(String(&const_cast<AtomicStringImpl&>(name)).lower().impl() == &name);
450 return m_lowercasedUsemap.impl() == &name;
453 const AtomicString& HTMLImageElement::alt() const
455 return fastGetAttribute(altAttr);
458 bool HTMLImageElement::draggable() const
460 // Image elements are draggable by default.
461 return !equalIgnoringCase(fastGetAttribute(draggableAttr), "false");
464 void HTMLImageElement::setHeight(int value)
466 setIntegralAttribute(heightAttr, value);
469 URL HTMLImageElement::src() const
471 return document().completeURL(fastGetAttribute(srcAttr));
474 void HTMLImageElement::setSrc(const String& value)
476 setAttribute(srcAttr, value);
479 void HTMLImageElement::setWidth(int value)
481 setIntegralAttribute(widthAttr, value);
484 int HTMLImageElement::x() const
486 document().updateLayoutIgnorePendingStylesheets();
487 auto renderer = this->renderer();
491 // FIXME: This doesn't work correctly with transforms.
492 return renderer->localToAbsolute().x();
495 int HTMLImageElement::y() const
497 document().updateLayoutIgnorePendingStylesheets();
498 auto renderer = this->renderer();
502 // FIXME: This doesn't work correctly with transforms.
503 return renderer->localToAbsolute().y();
506 bool HTMLImageElement::complete() const
508 return m_imageLoader.imageComplete();
511 void HTMLImageElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const
513 HTMLElement::addSubresourceAttributeURLs(urls);
515 addSubresourceURL(urls, src());
516 // FIXME: What about when the usemap attribute begins with "#"?
517 addSubresourceURL(urls, document().completeURL(fastGetAttribute(usemapAttr)));
520 void HTMLImageElement::didMoveToNewDocument(Document* oldDocument)
522 m_imageLoader.elementDidMoveToNewDocument();
523 HTMLElement::didMoveToNewDocument(oldDocument);
526 bool HTMLImageElement::isServerMap() const
528 if (!fastHasAttribute(ismapAttr))
531 const AtomicString& usemap = fastGetAttribute(usemapAttr);
533 // If the usemap attribute starts with '#', it refers to a map element in the document.
534 if (usemap.string()[0] == '#')
537 return document().completeURL(stripLeadingAndTrailingHTMLSpaces(usemap)).isEmpty();
540 #if ENABLE(SERVICE_CONTROLS)
541 void HTMLImageElement::updateImageControls()
543 // If this image element is inside a shadow tree then it is part of an image control.
544 if (isInShadowTree())
547 Settings* settings = document().settings();
548 if (!settings || !settings->imageControlsEnabled())
551 bool hasControls = hasImageControls();
552 if (!m_experimentalImageMenuEnabled && hasControls)
553 destroyImageControls();
554 else if (m_experimentalImageMenuEnabled && !hasControls)
555 createImageControls();
558 void HTMLImageElement::createImageControls()
560 ASSERT(m_experimentalImageMenuEnabled);
561 ASSERT(!hasImageControls());
563 RefPtr<ImageControlsRootElement> imageControls = ImageControlsRootElement::maybeCreate(document());
567 ensureUserAgentShadowRoot().appendChild(imageControls.releaseNonNull());
569 auto* renderObject = renderer();
573 downcast<RenderImage>(*renderObject).setHasShadowControls(true);
576 void HTMLImageElement::destroyImageControls()
578 ShadowRoot* shadowRoot = userAgentShadowRoot();
582 if (Node* node = shadowRoot->firstChild()) {
583 ASSERT_WITH_SECURITY_IMPLICATION(node->isImageControlsRootElement());
584 shadowRoot->removeChild(*node);
587 auto* renderObject = renderer();
591 downcast<RenderImage>(*renderObject).setHasShadowControls(false);
594 bool HTMLImageElement::hasImageControls() const
596 if (ShadowRoot* shadowRoot = userAgentShadowRoot()) {
597 Node* node = shadowRoot->firstChild();
598 ASSERT_WITH_SECURITY_IMPLICATION(!node || node->isImageControlsRootElement());
605 bool HTMLImageElement::childShouldCreateRenderer(const Node& child) const
607 return hasShadowRootParent(child) && HTMLElement::childShouldCreateRenderer(child);
609 #endif // ENABLE(SERVICE_CONTROLS)
612 // FIXME: This is a workaround for <rdar://problem/7725158>. We should find a better place for the touchCalloutEnabled() logic.
613 bool HTMLImageElement::willRespondToMouseClickEvents()
615 auto renderer = this->renderer();
616 if (!renderer || renderer->style().touchCalloutEnabled())
618 return HTMLElement::willRespondToMouseClickEvents();