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