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 "Attribute.h" 27#include "CSSPropertyNames.h" 28#include "CSSValueKeywords.h" 29#include "CachedImage.h" 30#include "EventNames.h" 31#include "FrameView.h" 32#include "HTMLAnchorElement.h" 33#include "HTMLDocument.h" 34#include "HTMLFormElement.h" 35#include "HTMLParserIdioms.h" 36#include "HTMLSrcsetParser.h" 37#include "Page.h" 38#include "RenderImage.h" 39#include "Settings.h" 40#include "ShadowRoot.h" 41#include "SourceSizeList.h" 42 43#if ENABLE(SERVICE_CONTROLS) 44#include "ImageControlsRootElement.h" 45#endif 46 47namespace WebCore { 48 49using namespace HTMLNames; 50 51HTMLImageElement::HTMLImageElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form) 52 : HTMLElement(tagName, document) 53 , m_imageLoader(*this) 54 , m_form(form) 55 , m_compositeOperator(CompositeSourceOver) 56 , m_imageDevicePixelRatio(1.0f) 57#if ENABLE(SERVICE_CONTROLS) 58 , m_experimentalImageMenuEnabled(false) 59#endif 60{ 61 ASSERT(hasTagName(imgTag)); 62 setHasCustomStyleResolveCallbacks(); 63 if (form) 64 form->registerImgElement(this); 65} 66 67PassRefPtr<HTMLImageElement> HTMLImageElement::create(Document& document) 68{ 69 return adoptRef(new HTMLImageElement(imgTag, document)); 70} 71 72PassRefPtr<HTMLImageElement> HTMLImageElement::create(const QualifiedName& tagName, Document& document, HTMLFormElement* form) 73{ 74 return adoptRef(new HTMLImageElement(tagName, document, form)); 75} 76 77HTMLImageElement::~HTMLImageElement() 78{ 79 if (m_form) 80 m_form->removeImgElement(this); 81} 82 83PassRefPtr<HTMLImageElement> HTMLImageElement::createForJSConstructor(Document& document, const int* optionalWidth, const int* optionalHeight) 84{ 85 RefPtr<HTMLImageElement> image = adoptRef(new HTMLImageElement(imgTag, document)); 86 if (optionalWidth) 87 image->setWidth(*optionalWidth); 88 if (optionalHeight) 89 image->setHeight(*optionalHeight); 90 return image.release(); 91} 92 93bool HTMLImageElement::isPresentationAttribute(const QualifiedName& name) const 94{ 95 if (name == widthAttr || name == heightAttr || name == borderAttr || name == vspaceAttr || name == hspaceAttr || name == alignAttr || name == valignAttr) 96 return true; 97 return HTMLElement::isPresentationAttribute(name); 98} 99 100void HTMLImageElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style) 101{ 102 if (name == widthAttr) 103 addHTMLLengthToStyle(style, CSSPropertyWidth, value); 104 else if (name == heightAttr) 105 addHTMLLengthToStyle(style, CSSPropertyHeight, value); 106 else if (name == borderAttr) 107 applyBorderAttributeToStyle(value, style); 108 else if (name == vspaceAttr) { 109 addHTMLLengthToStyle(style, CSSPropertyMarginTop, value); 110 addHTMLLengthToStyle(style, CSSPropertyMarginBottom, value); 111 } else if (name == hspaceAttr) { 112 addHTMLLengthToStyle(style, CSSPropertyMarginLeft, value); 113 addHTMLLengthToStyle(style, CSSPropertyMarginRight, value); 114 } else if (name == alignAttr) 115 applyAlignmentAttributeToStyle(value, style); 116 else if (name == valignAttr) 117 addPropertyToPresentationAttributeStyle(style, CSSPropertyVerticalAlign, value); 118 else 119 HTMLElement::collectStyleForPresentationAttribute(name, value, style); 120} 121 122const AtomicString& HTMLImageElement::imageSourceURL() const 123{ 124 return m_bestFitImageURL.isEmpty() ? fastGetAttribute(srcAttr) : m_bestFitImageURL; 125} 126 127void HTMLImageElement::setBestFitURLAndDPRFromImageCandidate(const ImageCandidate& candidate) 128{ 129 m_bestFitImageURL = candidate.string.toString(); 130#if ENABLE(PICTURE_SIZES) 131 m_currentSrc = AtomicString(document().completeURL(imageSourceURL()).string()); 132#endif 133 if (candidate.density >= 0) 134 m_imageDevicePixelRatio = 1 / candidate.density; 135 if (renderer() && renderer()->isImage()) 136 toRenderImage(renderer())->setImageDevicePixelRatio(m_imageDevicePixelRatio); 137} 138 139void HTMLImageElement::parseAttribute(const QualifiedName& name, const AtomicString& value) 140{ 141 if (name == altAttr) { 142 if (renderer() && renderer()->isRenderImage()) 143 toRenderImage(renderer())->updateAltText(); 144 } else if (name == srcAttr || name == srcsetAttr) { 145 ImageCandidate candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), fastGetAttribute(srcAttr), fastGetAttribute(srcsetAttr) 146#if ENABLE(PICTURE_SIZES) 147 , SourceSizeList::parseSizesAttribute(fastGetAttribute(sizesAttr), document().renderView(), document().frame()) 148#endif 149 ); 150 setBestFitURLAndDPRFromImageCandidate(candidate); 151 m_imageLoader.updateFromElementIgnoringPreviousError(); 152 } else if (name == usemapAttr) { 153 setIsLink(!value.isNull() && !shouldProhibitLinks(this)); 154 155 if (inDocument() && !m_lowercasedUsemap.isNull()) 156 document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this); 157 158 // The HTMLImageElement's useMap() value includes the '#' symbol at the beginning, which has to be stripped off. 159 // FIXME: We should check that the first character is '#'. 160 // FIXME: HTML5 specification says we should strip any leading string before '#'. 161 // FIXME: HTML5 specification says we should ignore usemap attributes without #. 162 if (value.length() > 1) 163 m_lowercasedUsemap = value.string().substring(1).lower(); 164 else 165 m_lowercasedUsemap = nullAtom; 166 167 if (inDocument() && !m_lowercasedUsemap.isNull()) 168 document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this); 169 } else if (name == onbeforeloadAttr) 170 setAttributeEventListener(eventNames().beforeloadEvent, name, value); 171 else if (name == compositeAttr) { 172 // FIXME: images don't support blend modes in their compositing attribute. 173 BlendMode blendOp = BlendModeNormal; 174 if (!parseCompositeAndBlendOperator(value, m_compositeOperator, blendOp)) 175 m_compositeOperator = CompositeSourceOver; 176#if ENABLE(SERVICE_CONTROLS) 177 } else if (name == webkitimagemenuAttr) { 178 m_experimentalImageMenuEnabled = !value.isNull(); 179 updateImageControls(); 180#endif 181 } else { 182 if (name == nameAttr) { 183 bool willHaveName = !value.isNull(); 184 if (hasName() != willHaveName && inDocument() && document().isHTMLDocument()) { 185 HTMLDocument* document = toHTMLDocument(&this->document()); 186 const AtomicString& id = getIdAttribute(); 187 if (!id.isEmpty() && id != getNameAttribute()) { 188 if (willHaveName) 189 document->addDocumentNamedItem(*id.impl(), *this); 190 else 191 document->removeDocumentNamedItem(*id.impl(), *this); 192 } 193 } 194 } 195 HTMLElement::parseAttribute(name, value); 196 } 197} 198 199String HTMLImageElement::altText() const 200{ 201 // lets figure out the alt text.. magic stuff 202 // http://www.w3.org/TR/1998/REC-html40-19980424/appendix/notes.html#altgen 203 // also heavily discussed by Hixie on bugzilla 204 String alt = getAttribute(altAttr); 205 // fall back to title attribute 206 if (alt.isNull()) 207 alt = getAttribute(titleAttr); 208 return alt; 209} 210 211RenderPtr<RenderElement> HTMLImageElement::createElementRenderer(PassRef<RenderStyle> style) 212{ 213 if (style.get().hasContent()) 214 return RenderElement::createFor(*this, WTF::move(style)); 215 216 return createRenderer<RenderImage>(*this, WTF::move(style), nullptr, m_imageDevicePixelRatio); 217} 218 219bool HTMLImageElement::canStartSelection() const 220{ 221 if (shadowRoot()) 222 return HTMLElement::canStartSelection(); 223 224 return false; 225} 226 227void HTMLImageElement::didAttachRenderers() 228{ 229 if (!renderer() || !renderer()->isRenderImage()) 230 return; 231 if (m_imageLoader.hasPendingBeforeLoadEvent()) 232 return; 233 234#if ENABLE(SERVICE_CONTROLS) 235 updateImageControls(); 236#endif 237 238 RenderImage* renderImage = toRenderImage(renderer()); 239 RenderImageResource& renderImageResource = renderImage->imageResource(); 240 if (renderImageResource.hasImage()) 241 return; 242 renderImageResource.setCachedImage(m_imageLoader.image()); 243 244 // If we have no image at all because we have no src attribute, set 245 // image height and width for the alt text instead. 246 if (!m_imageLoader.image() && !renderImageResource.cachedImage()) 247 renderImage->setImageSizeForAltText(); 248} 249 250Node::InsertionNotificationRequest HTMLImageElement::insertedInto(ContainerNode& insertionPoint) 251{ 252 if (!m_form) { // m_form can be non-null if it was set in constructor. 253 m_form = HTMLFormElement::findClosestFormAncestor(*this); 254 if (m_form) 255 m_form->registerImgElement(this); 256 } 257 258 // Insert needs to complete first, before we start updating the loader. Loader dispatches events which could result 259 // in callbacks back to this node. 260 Node::InsertionNotificationRequest insertNotificationRequest = HTMLElement::insertedInto(insertionPoint); 261 262 if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull()) 263 document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this); 264 265 // If we have been inserted from a renderer-less document, 266 // our loader may have not fetched the image, so do it now. 267 if (insertionPoint.inDocument() && !m_imageLoader.image()) 268 m_imageLoader.updateFromElement(); 269 270 return insertNotificationRequest; 271} 272 273void HTMLImageElement::removedFrom(ContainerNode& insertionPoint) 274{ 275 if (m_form) 276 m_form->removeImgElement(this); 277 278 if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull()) 279 document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this); 280 281 m_form = 0; 282 HTMLElement::removedFrom(insertionPoint); 283} 284 285int HTMLImageElement::width(bool ignorePendingStylesheets) 286{ 287 if (!renderer()) { 288 // check the attribute first for an explicit pixel value 289 bool ok; 290 int width = getAttribute(widthAttr).toInt(&ok); 291 if (ok) 292 return width; 293 294 // if the image is available, use its width 295 if (m_imageLoader.image()) 296 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width(); 297 } 298 299 if (ignorePendingStylesheets) 300 document().updateLayoutIgnorePendingStylesheets(); 301 else 302 document().updateLayout(); 303 304 RenderBox* box = renderBox(); 305 return box ? adjustForAbsoluteZoom(box->contentBoxRect().pixelSnappedWidth(), *box) : 0; 306} 307 308int HTMLImageElement::height(bool ignorePendingStylesheets) 309{ 310 if (!renderer()) { 311 // check the attribute first for an explicit pixel value 312 bool ok; 313 int height = getAttribute(heightAttr).toInt(&ok); 314 if (ok) 315 return height; 316 317 // if the image is available, use its height 318 if (m_imageLoader.image()) 319 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height(); 320 } 321 322 if (ignorePendingStylesheets) 323 document().updateLayoutIgnorePendingStylesheets(); 324 else 325 document().updateLayout(); 326 327 RenderBox* box = renderBox(); 328 return box ? adjustForAbsoluteZoom(box->contentBoxRect().pixelSnappedHeight(), *box) : 0; 329} 330 331int HTMLImageElement::naturalWidth() const 332{ 333 if (!m_imageLoader.image()) 334 return 0; 335 336 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width(); 337} 338 339int HTMLImageElement::naturalHeight() const 340{ 341 if (!m_imageLoader.image()) 342 return 0; 343 344 return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height(); 345} 346 347bool HTMLImageElement::isURLAttribute(const Attribute& attribute) const 348{ 349 return attribute.name() == srcAttr 350 || attribute.name() == lowsrcAttr 351 || attribute.name() == longdescAttr 352 || (attribute.name() == usemapAttr && attribute.value().string()[0] != '#') 353 || HTMLElement::isURLAttribute(attribute); 354} 355 356bool HTMLImageElement::matchesLowercasedUsemap(const AtomicStringImpl& name) const 357{ 358 ASSERT(String(&const_cast<AtomicStringImpl&>(name)).lower().impl() == &name); 359 return m_lowercasedUsemap.impl() == &name; 360} 361 362const AtomicString& HTMLImageElement::alt() const 363{ 364 return getAttribute(altAttr); 365} 366 367bool HTMLImageElement::draggable() const 368{ 369 // Image elements are draggable by default. 370 return !equalIgnoringCase(getAttribute(draggableAttr), "false"); 371} 372 373void HTMLImageElement::setHeight(int value) 374{ 375 setIntegralAttribute(heightAttr, value); 376} 377 378URL HTMLImageElement::src() const 379{ 380 return document().completeURL(getAttribute(srcAttr)); 381} 382 383void HTMLImageElement::setSrc(const String& value) 384{ 385 setAttribute(srcAttr, value); 386} 387 388void HTMLImageElement::setWidth(int value) 389{ 390 setIntegralAttribute(widthAttr, value); 391} 392 393int HTMLImageElement::x() const 394{ 395 document().updateLayoutIgnorePendingStylesheets(); 396 auto renderer = this->renderer(); 397 if (!renderer) 398 return 0; 399 400 // FIXME: This doesn't work correctly with transforms. 401 return renderer->localToAbsolute().x(); 402} 403 404int HTMLImageElement::y() const 405{ 406 document().updateLayoutIgnorePendingStylesheets(); 407 auto renderer = this->renderer(); 408 if (!renderer) 409 return 0; 410 411 // FIXME: This doesn't work correctly with transforms. 412 return renderer->localToAbsolute().y(); 413} 414 415bool HTMLImageElement::complete() const 416{ 417 return m_imageLoader.imageComplete(); 418} 419 420void HTMLImageElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const 421{ 422 HTMLElement::addSubresourceAttributeURLs(urls); 423 424 addSubresourceURL(urls, src()); 425 // FIXME: What about when the usemap attribute begins with "#"? 426 addSubresourceURL(urls, document().completeURL(getAttribute(usemapAttr))); 427} 428 429void HTMLImageElement::didMoveToNewDocument(Document* oldDocument) 430{ 431 m_imageLoader.elementDidMoveToNewDocument(); 432 HTMLElement::didMoveToNewDocument(oldDocument); 433} 434 435bool HTMLImageElement::isServerMap() const 436{ 437 if (!fastHasAttribute(ismapAttr)) 438 return false; 439 440 const AtomicString& usemap = fastGetAttribute(usemapAttr); 441 442 // If the usemap attribute starts with '#', it refers to a map element in the document. 443 if (usemap.string()[0] == '#') 444 return false; 445 446 return document().completeURL(stripLeadingAndTrailingHTMLSpaces(usemap)).isEmpty(); 447} 448 449#if ENABLE(SERVICE_CONTROLS) 450void HTMLImageElement::updateImageControls() 451{ 452 // If this image element is inside a shadow tree then it is part of an image control. 453 if (isInShadowTree()) 454 return; 455 456 Settings* settings = document().settings(); 457 if (!settings || !settings->imageControlsEnabled()) 458 return; 459 460 bool hasControls = hasImageControls(); 461 if (!m_experimentalImageMenuEnabled && hasControls) 462 destroyImageControls(); 463 else if (m_experimentalImageMenuEnabled && !hasControls) 464 createImageControls(); 465} 466 467void HTMLImageElement::createImageControls() 468{ 469 ASSERT(m_experimentalImageMenuEnabled); 470 ASSERT(!hasImageControls()); 471 472 RefPtr<ImageControlsRootElement> imageControls = ImageControlsRootElement::maybeCreate(document()); 473 if (!imageControls) 474 return; 475 476 ensureUserAgentShadowRoot().appendChild(imageControls); 477 478 RenderObject* renderObject = renderer(); 479 if (!renderObject) 480 return; 481 482 toRenderImage(renderObject)->setHasShadowControls(true); 483} 484 485void HTMLImageElement::destroyImageControls() 486{ 487 ShadowRoot* shadowRoot = userAgentShadowRoot(); 488 if (!shadowRoot) 489 return; 490 491 if (Node* node = shadowRoot->firstChild()) { 492 ASSERT_WITH_SECURITY_IMPLICATION(node->isImageControlsRootElement()); 493 shadowRoot->removeChild(node); 494 } 495 496 RenderObject* renderObject = renderer(); 497 if (!renderObject) 498 return; 499 500 toRenderImage(renderObject)->setHasShadowControls(false); 501} 502 503bool HTMLImageElement::hasImageControls() const 504{ 505 if (ShadowRoot* shadowRoot = userAgentShadowRoot()) { 506 Node* node = shadowRoot->firstChild(); 507 ASSERT_WITH_SECURITY_IMPLICATION(!node || node->isImageControlsRootElement()); 508 return node; 509 } 510 511 return false; 512} 513 514bool HTMLImageElement::childShouldCreateRenderer(const Node& child) const 515{ 516 return hasShadowRootParent(child) && HTMLElement::childShouldCreateRenderer(child); 517} 518#endif // ENABLE(SERVICE_CONTROLS) 519 520#if PLATFORM(IOS) 521// FIXME: This is a workaround for <rdar://problem/7725158>. We should find a better place for the touchCalloutEnabled() logic. 522bool HTMLImageElement::willRespondToMouseClickEvents() 523{ 524 auto renderer = this->renderer(); 525 if (!renderer || renderer->style().touchCalloutEnabled()) 526 return true; 527 return HTMLElement::willRespondToMouseClickEvents(); 528} 529#endif 530 531} 532