blob: 75c995b68b85120079026964701a4ff082294962 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/html/html_image_fallback_helper.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/dom/element_rare_data.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_image_loader.h"
#include "third_party/blink/renderer/core/html/html_span_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
namespace blink {
static bool ElementRepresentsNothing(const Element& element) {
const auto& html_element = To<HTMLElement>(element);
// We source fallback content/alternative text from more than just the 'alt'
// attribute, so consider the element to represent text in those cases as
// well.
bool alt_is_set = !html_element.AltText().IsNull();
bool alt_is_empty = alt_is_set && html_element.AltText().IsEmpty();
bool src_is_set = !element.getAttribute(html_names::kSrcAttr).IsEmpty();
if (src_is_set && alt_is_empty)
return true;
return !src_is_set && (!alt_is_set || alt_is_empty);
}
static bool ImageSmallerThanAltImage(int pixels_for_alt_image,
const Length& width,
const Length& height) {
// We don't have a layout tree so can't compute the size of an image
// relative dimensions - so we just assume we should display the alt image.
if (!width.IsFixed() && !height.IsFixed())
return false;
if (height.IsFixed() && height.Value() < pixels_for_alt_image)
return true;
return width.IsFixed() && width.Value() < pixels_for_alt_image;
}
namespace {
class ImageFallbackContentBuilder {
STACK_ALLOCATED();
public:
ImageFallbackContentBuilder(const ShadowRoot& shadow_root)
: place_holder_(shadow_root.getElementById("alttext-container")),
broken_image_(shadow_root.getElementById("alttext-image")) {}
bool HasContentElements() const { return place_holder_ && broken_image_; }
void ShowBrokenImageIcon(bool is_ltr) {
broken_image_->SetInlineStyleProperty(CSSPropertyID::kDisplay,
CSSValueID::kInline);
// Make sure the broken image icon appears on the appropriate side of the
// image for the element's writing direction.
broken_image_->SetInlineStyleProperty(
CSSPropertyID::kFloat, AtomicString(is_ltr ? "left" : "right"));
}
void HideBrokenImageIcon() {
broken_image_->SetInlineStyleProperty(CSSPropertyID::kDisplay,
CSSValueID::kNone);
}
void ShowAsReplaced(const Length& width, const Length& height, float zoom) {
place_holder_->SetInlineStyleProperty(CSSPropertyID::kOverflow,
CSSValueID::kHidden);
place_holder_->SetInlineStyleProperty(CSSPropertyID::kDisplay,
CSSValueID::kInlineBlock);
place_holder_->SetInlineStyleProperty(CSSPropertyID::kPointerEvents,
CSSValueID::kNone);
place_holder_->SetInlineStyleProperty(CSSPropertyID::kHeight,
*CSSValue::Create(height, zoom));
place_holder_->SetInlineStyleProperty(CSSPropertyID::kWidth,
*CSSValue::Create(width, zoom));
}
void ShowBorder() {
place_holder_->SetInlineStyleProperty(CSSPropertyID::kBorderWidth, 1,
CSSPrimitiveValue::UnitType::kPixels);
place_holder_->SetInlineStyleProperty(CSSPropertyID::kBorderStyle,
CSSValueID::kSolid);
place_holder_->SetInlineStyleProperty(CSSPropertyID::kBorderColor,
CSSValueID::kSilver);
place_holder_->SetInlineStyleProperty(CSSPropertyID::kPadding, 1,
CSSPrimitiveValue::UnitType::kPixels);
place_holder_->SetInlineStyleProperty(CSSPropertyID::kBoxSizing,
CSSValueID::kBorderBox);
}
void AlignToBaseline() {
place_holder_->SetInlineStyleProperty(CSSPropertyID::kVerticalAlign,
CSSValueID::kBaseline);
}
private:
Element* place_holder_;
Element* broken_image_;
};
} // namespace
void HTMLImageFallbackHelper::CreateAltTextShadowTree(Element& element) {
Document& document = element.GetDocument();
auto* container = MakeGarbageCollected<HTMLSpanElement>(document);
container->setAttribute(html_names::kIdAttr,
AtomicString("alttext-container"));
auto* broken_image = MakeGarbageCollected<HTMLImageElement>(document);
broken_image->SetIsFallbackImage();
broken_image->setAttribute(html_names::kIdAttr,
AtomicString("alttext-image"));
broken_image->setAttribute(html_names::kWidthAttr, AtomicString("16"));
broken_image->setAttribute(html_names::kHeightAttr, AtomicString("16"));
broken_image->setAttribute(html_names::kAlignAttr, AtomicString("left"));
broken_image->SetInlineStyleProperty(CSSPropertyID::kMargin, 0,
CSSPrimitiveValue::UnitType::kPixels);
container->AppendChild(broken_image);
auto* alt_text = MakeGarbageCollected<HTMLSpanElement>(document);
alt_text->setAttribute(html_names::kIdAttr, AtomicString("alttext"));
auto* text = Text::Create(document, To<HTMLElement>(element).AltText());
alt_text->AppendChild(text);
container->AppendChild(alt_text);
element.EnsureUserAgentShadowRoot().AppendChild(container);
}
void HTMLImageFallbackHelper::CustomStyleForAltText(Element& element,
ComputedStyle& new_style) {
// If we have an author shadow root or have not created the UA shadow root
// yet, bail early. We can't use ensureUserAgentShadowRoot() here because that
// would alter the DOM tree during style recalc.
if (element.AuthorShadowRoot() || !element.UserAgentShadowRoot())
return;
ImageFallbackContentBuilder fallback(*element.UserAgentShadowRoot());
// Input elements have a UA shadow root of their own. We may not have replaced
// it with fallback content yet.
if (!fallback.HasContentElements())
return;
// This method is called during style recalc, and it is generally not allowed
// to mark nodes style dirty during recalc. The code below modifies inline
// style in the UA shadow tree below based on the computed style for the image
// element. As part of that we mark elements in the shadow tree style dirty.
// The scope object here is to allow that and avoid DCHECK failures which
// would otherwise have been triggered.
StyleEngine::AllowMarkStyleDirtyFromRecalcScope scope(
element.GetDocument().GetStyleEngine());
if (element.GetDocument().InQuirksMode()) {
// Mimic the behaviour of the image host by setting symmetric dimensions if
// only one dimension is specified.
if (!new_style.Width().IsAuto() && new_style.Height().IsAuto())
new_style.SetHeight(new_style.Width());
else if (!new_style.Height().IsAuto() && new_style.Width().IsAuto())
new_style.SetWidth(new_style.Height());
if (!new_style.Width().IsAuto() && !new_style.Height().IsAuto())
fallback.AlignToBaseline();
}
bool has_intrinsic_dimensions =
!new_style.Width().IsAuto() && !new_style.Height().IsAuto();
bool has_dimensions_from_ar =
!new_style.AspectRatio().IsAuto() &&
(!new_style.Width().IsAuto() || !new_style.Height().IsAuto());
bool has_no_alt_attribute =
element.getAttribute(html_names::kAltAttr).IsEmpty();
bool treat_as_replaced =
(has_intrinsic_dimensions || has_dimensions_from_ar) &&
(element.GetDocument().InQuirksMode() || has_no_alt_attribute);
if (treat_as_replaced) {
// https://html.spec.whatwg.org/C/#images-3:
// "If the element does not represent an image, but the element already has
// intrinsic dimensions (e.g. from the dimension attributes or CSS rules),
// and either: the user agent has reason to believe that the image will
// become available and be rendered in due course, or the element has no alt
// attribute, or the Document is in quirks mode The user agent is expected
// to treat the element as a replaced element whose content is the text that
// the element represents, if any."
fallback.ShowAsReplaced(new_style.Width(), new_style.Height(),
new_style.EffectiveZoom());
// 16px for the image and 2px for its top/left border/padding offset.
int pixels_for_alt_image = 18;
if (ImageSmallerThanAltImage(pixels_for_alt_image, new_style.Width(),
new_style.Height())) {
fallback.HideBrokenImageIcon();
} else {
fallback.ShowBorder();
fallback.ShowBrokenImageIcon(new_style.IsLeftToRightDirection());
}
} else {
if (new_style.Display() == EDisplay::kInline) {
new_style.SetWidth(Length());
new_style.SetHeight(Length());
new_style.SetAspectRatio(
ComputedStyleInitialValues::InitialAspectRatio());
}
if (ElementRepresentsNothing(element)) {
// "If the element is an img element that represents nothing and the user
// agent does not expect this to change the user agent is expected to
// treat the element as an empty inline element."
// - We achieve this by hiding the broken image so that the span is
// empty.
fallback.HideBrokenImageIcon();
} else {
// "If the element is an img element that represents some text and the
// user agent does not expect this to change the user agent is expected to
// treat the element as a non-replaced phrasing element whose content is
// the text, optionally with an icon indicating that an image is missing,
// so that the user can request the image be displayed or investigate why
// it is not rendering."
fallback.ShowBrokenImageIcon(new_style.IsLeftToRightDirection());
}
}
}
} // namespace blink