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