ladybird/Libraries/LibWeb/HTML/HTMLObjectElement.cpp
Andreas Kling b981e6f7bc LibWeb: Avoid many style invalidations on DOM attribute mutation
Many times, attribute mutation doesn't necessitate a full style
invalidation on the element. However, the conditions are pretty
elaborate, so this first version has a lot of false positives.

We only need to invalidate style when any of these things apply:

1. The change may affect the match state of a selector somewhere.
2. The change may affect presentational hints applied to the element.

For (1) in this first version, we have a fixed list of attribute names
that may affect selectors. We also collect all names referenced by
attribute selectors anywhere in the document.

For (2), we add a new Element::is_presentational_hint() virtual that
tells us whether a given attribute name is a presentational hint.

This drastically reduces style work on many websites. As an example,
https://cnn.com/ is once again browseable.
2024-12-24 17:17:09 +01:00

597 lines
28 KiB
C++

/*
* Copyright (c) 2020-2024, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Bitmap.h>
#include <LibWeb/Bindings/HTMLObjectElementPrototype.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
#include <LibWeb/DOM/DocumentLoading.h>
#include <LibWeb/DOM/DocumentObserver.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/HTML/DecodedImageData.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/HTMLObjectElement.h>
#include <LibWeb/HTML/ImageRequest.h>
#include <LibWeb/HTML/Numbers.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/PotentialCORSRequest.h>
#include <LibWeb/Layout/ImageBox.h>
#include <LibWeb/Layout/NavigableContainerViewport.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/MimeSniff/Resource.h>
namespace Web::HTML {
GC_DEFINE_ALLOCATOR(HTMLObjectElement);
HTMLObjectElement::HTMLObjectElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: NavigableContainer(document, move(qualified_name))
{
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element:potentially-delays-the-load-event
set_potentially_delays_the_load_event(true);
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element
// Whenever one of the following conditions occur:
// - the element is created,
// ...the user agent must queue an element task on the DOM manipulation task source given the object element to run
// the following steps to (re)determine what the object element represents.
queue_element_task_to_run_object_representation_steps();
}
HTMLObjectElement::~HTMLObjectElement() = default;
void HTMLObjectElement::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLObjectElement);
m_document_observer = realm.create<DOM::DocumentObserver>(realm, document());
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element
// Whenever one of the following conditions occur:
// - the element's node document changes whether it is fully active,
// ...the user agent must queue an element task on the DOM manipulation task source given the object element to run
// the following steps to (re)determine what the object element represents.
m_document_observer->set_document_became_active([this]() {
queue_element_task_to_run_object_representation_steps();
});
m_document_observer->set_document_became_inactive([this]() {
queue_element_task_to_run_object_representation_steps();
});
}
void HTMLObjectElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_resource_request);
visitor.visit(m_document_observer);
}
void HTMLObjectElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<FlyString> const&)
{
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element
// Whenever one of the following conditions occur:
if (
// - the element's classid attribute is set, changed, or removed,
(name == HTML::AttributeNames::classid) ||
// - the element's classid attribute is not present, and its data attribute is set, changed, or removed,
(!has_attribute(HTML::AttributeNames::classid) && name == HTML::AttributeNames::data) ||
// - neither the element's classid attribute nor its data attribute are present, and its type attribute is set, changed, or removed,
(!has_attribute(HTML::AttributeNames::classid) && !has_attribute(HTML::AttributeNames::data) && name == HTML::AttributeNames::type)) {
// ...the user agent must queue an element task on the DOM manipulation task source given the object element to run
// the following steps to (re)determine what the object element represents.
queue_element_task_to_run_object_representation_steps();
}
}
void HTMLObjectElement::form_associated_element_was_removed(DOM::Node*)
{
destroy_the_child_navigable();
}
bool HTMLObjectElement::is_presentational_hint(FlyString const& name) const
{
if (Base::is_presentational_hint(name))
return true;
return first_is_one_of(name,
HTML::AttributeNames::align,
HTML::AttributeNames::border,
HTML::AttributeNames::height,
HTML::AttributeNames::hspace,
HTML::AttributeNames::vspace,
HTML::AttributeNames::width);
}
void HTMLObjectElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const
{
for_each_attribute([&](auto& name, auto& value) {
if (name == HTML::AttributeNames::align) {
if (value.equals_ignoring_ascii_case("center"sv))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::TextAlign, CSS::CSSKeywordValue::create(CSS::Keyword::Center));
else if (value.equals_ignoring_ascii_case("middle"sv))
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::TextAlign, CSS::CSSKeywordValue::create(CSS::Keyword::Middle));
} else if (name == HTML::AttributeNames::border) {
if (auto parsed_value = parse_non_negative_integer(value); parsed_value.has_value()) {
auto width_style_value = CSS::LengthStyleValue::create(CSS::Length::make_px(*parsed_value));
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderTopWidth, width_style_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderRightWidth, width_style_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderBottomWidth, width_style_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderLeftWidth, width_style_value);
auto border_style_value = CSS::CSSKeywordValue::create(CSS::Keyword::Solid);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderTopStyle, border_style_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderRightStyle, border_style_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderBottomStyle, border_style_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::BorderLeftStyle, border_style_value);
}
}
// https://html.spec.whatwg.org/multipage/rendering.html#attributes-for-embedded-content-and-images:maps-to-the-dimension-property-3
else if (name == HTML::AttributeNames::height) {
if (auto parsed_value = parse_dimension_value(value)) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Height, *parsed_value);
}
}
// https://html.spec.whatwg.org/multipage/rendering.html#attributes-for-embedded-content-and-images:maps-to-the-dimension-property
else if (name == HTML::AttributeNames::hspace) {
if (auto parsed_value = parse_dimension_value(value)) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::MarginLeft, *parsed_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::MarginRight, *parsed_value);
}
} else if (name == HTML::AttributeNames::vspace) {
if (auto parsed_value = parse_dimension_value(value)) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::MarginTop, *parsed_value);
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::MarginBottom, *parsed_value);
}
} else if (name == HTML::AttributeNames::width) {
if (auto parsed_value = parse_dimension_value(value)) {
cascaded_properties->set_property_from_presentational_hint(CSS::PropertyID::Width, *parsed_value);
}
}
});
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-object-data
String HTMLObjectElement::data() const
{
auto data = get_attribute(HTML::AttributeNames::data);
if (!data.has_value())
return {};
return document().encoding_parse_url(*data).to_string();
}
GC::Ptr<Layout::Node> HTMLObjectElement::create_layout_node(GC::Ref<CSS::ComputedProperties> style)
{
switch (m_representation) {
case Representation::Children:
return NavigableContainer::create_layout_node(move(style));
case Representation::ContentNavigable:
return heap().allocate<Layout::NavigableContainerViewport>(document(), *this, move(style));
case Representation::Image:
if (image_data())
return heap().allocate<Layout::ImageBox>(document(), *this, move(style), *this);
break;
default:
break;
}
return nullptr;
}
void HTMLObjectElement::adjust_computed_style(CSS::ComputedProperties& style)
{
// https://drafts.csswg.org/css-display-3/#unbox
if (style.display().is_contents())
style.set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::None)));
}
bool HTMLObjectElement::has_ancestor_media_element_or_object_element_not_showing_fallback_content() const
{
for (auto const* ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
if (is<HTMLMediaElement>(*ancestor))
return true;
if (is<HTMLObjectElement>(*ancestor)) {
auto& ancestor_object = static_cast<HTMLObjectElement const&>(*ancestor);
if (ancestor_object.m_representation != Representation::Children)
return true;
}
}
return false;
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element:queue-an-element-task
void HTMLObjectElement::queue_element_task_to_run_object_representation_steps()
{
// AD-HOC: If the document isn't fully active, this task will never run, and we will indefinitely delay the load event.
if (!document().is_fully_active())
return;
// This task being queued or actively running must delay the load event of the element's node document.
m_document_load_event_delayer_for_object_representation_task.emplace(document());
queue_an_element_task(HTML::Task::Source::DOMManipulation, [this]() {
ScopeGuard guard { [&]() { m_document_load_event_delayer_for_object_representation_task.clear(); } };
auto& realm = this->realm();
auto& vm = realm.vm();
// FIXME: 1. If the user has indicated a preference that this object element's fallback content be shown instead of the
// element's usual behavior, then jump to the step below labeled fallback.
// 2. If the element has an ancestor media element, or has an ancestor object element that is not showing its
// fallback content, or if the element is not in a document whose browsing context is non-null, or if the
// element's node document is not fully active, or if the element is still in the stack of open elements of
// an HTML parser or XML parser, or if the element is not being rendered, then jump to the step below labeled
// fallback.
// FIXME: Handle the element being in the stack of open elements.
// FIXME: Handle the element not being rendered.
if (!document().browsing_context() || !document().is_fully_active()) {
run_object_representation_fallback_steps();
return;
}
if (has_ancestor_media_element_or_object_element_not_showing_fallback_content()) {
run_object_representation_fallback_steps();
return;
}
// 3. If the data attribute is present and its value is not the empty string, then:
if (auto data = get_attribute(HTML::AttributeNames::data); data.has_value() && !data->is_empty()) {
// 1. If the type attribute is present and its value is not a type that the user agent supports, then the user
// agent may jump to the step below labeled fallback without fetching the content to examine its real type.
// 2. Let url be the result of encoding-parsing a URL given the data attribute's value, relative to the element's node document.
auto url = document().encoding_parse_url(*data);
// 3. If url is failure, then fire an event named error at the element and jump to the step below labeled fallback.
if (!url.is_valid()) {
dispatch_event(DOM::Event::create(realm, HTML::EventNames::error));
run_object_representation_fallback_steps();
return;
}
// 4. Let request be a new request whose URL is url, client is the element's node document's relevant settings
// object, destination is "object", credentials mode is "include", mode is "navigate", initiator type is
// "object", and whose use-URL-credentials flag is set.
auto request = Fetch::Infrastructure::Request::create(vm);
request->set_url(move(url));
request->set_client(&document().relevant_settings_object());
request->set_destination(Fetch::Infrastructure::Request::Destination::Object);
request->set_credentials_mode(Fetch::Infrastructure::Request::CredentialsMode::Include);
request->set_mode(Fetch::Infrastructure::Request::Mode::Navigate);
request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Object);
request->set_use_url_credentials(true);
Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
fetch_algorithms_input.process_response = [this](GC::Ref<Fetch::Infrastructure::Response> response) {
auto& realm = this->realm();
auto& global = document().realm().global_object();
if (response->is_network_error()) {
resource_did_fail();
return;
}
if (response->type() == Fetch::Infrastructure::Response::Type::Opaque || response->type() == Fetch::Infrastructure::Response::Type::OpaqueRedirect) {
auto& filtered_response = static_cast<Fetch::Infrastructure::FilteredResponse&>(*response);
response = filtered_response.internal_response();
}
auto on_data_read = GC::create_function(realm.heap(), [this, response](ByteBuffer data) {
resource_did_load(response, data);
});
auto on_error = GC::create_function(realm.heap(), [this](JS::Value) {
resource_did_fail();
});
VERIFY(response->body());
response->body()->fully_read(realm, on_data_read, on_error, GC::Ref { global });
};
// 5. Fetch request.
auto result = Fetch::Fetching::fetch(realm, request, Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input)));
if (result.is_error()) {
resource_did_fail();
return;
}
// Fetching the resource must delay the load event of the element's node document until the task that is
// queued by the networking task source once the resource has been fetched (defined next) has been run.
m_document_load_event_delayer_for_resource_load.emplace(document());
// 6. If the resource is not yet available (e.g. because the resource was not available in the cache, so that
// loading the resource required making a request over the network), then jump to the step below labeled
// fallback. The task that is queued by the networking task source once the resource is available must
// restart this algorithm from this step. Resources can load incrementally; user agents may opt to consider
// a resource "available" whenever enough data has been obtained to begin processing the resource.
// NOTE: The request is always asynchronous, even if it is cached or succeeded/failed immediately. Allow the
// response callback to invoke the fallback steps. This prevents the fallback layout from flashing very
// briefly between here and the resource loading.
}
});
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element:concept-event-fire-2
void HTMLObjectElement::resource_did_fail()
{
ScopeGuard guard { [&]() { m_document_load_event_delayer_for_resource_load.clear(); } };
// 3.7. If the load failed (e.g. there was an HTTP 404 error, there was a DNS error), fire an event named error at
// the element, then jump to the step below labeled fallback.
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error));
run_object_representation_fallback_steps();
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#object-type-detection
void HTMLObjectElement::resource_did_load(Fetch::Infrastructure::Response const& response, ReadonlyBytes data)
{
ScopeGuard guard { [&]() { m_document_load_event_delayer_for_resource_load.clear(); } };
// 3.8. Determine the resource type, as follows:
// 1. Let the resource type be unknown.
Optional<MimeSniff::MimeType> resource_type;
// FIXME: 3. If the user agent is configured to strictly obey Content-Type headers for this resource, and the resource has
// associated Content-Type metadata, then let the resource type be the type specified in the resource's Content-Type
// metadata, and jump to the step below labeled handler.
// 3. Run the appropriate set of steps from the following list:
// -> If the resource has associated Content-Type metadata
if (auto content_type = response.header_list()->extract_mime_type(); content_type.has_value()) {
// 1. Let binary be false.
bool binary = false;
// 2. If the type specified in the resource's Content-Type metadata is "text/plain", and the result of applying
// the rules for distinguishing if a resource is text or binary to the resource is that the resource is not
// text/plain, then set binary to true.
if (content_type->essence() == "text/plain"sv) {
auto computed_type = MimeSniff::Resource::sniff(
data,
MimeSniff::SniffingConfiguration {
.sniffing_context = MimeSniff::SniffingContext::TextOrBinary,
.supplied_type = content_type,
});
if (computed_type.essence() != "text/plain"sv)
binary = true;
}
// 3. If the type specified in the resource's Content-Type metadata is "application/octet-stream", then set binary to true.
else if (content_type->essence() == "application/octet-stream"sv) {
binary = true;
}
// 4. If binary is false, then let the resource type be the type specified in the resource's Content-Type metadata,
// and jump to the step below labeled handler.
if (!binary) {
resource_type = move(content_type);
}
// 5. If there is a type attribute present on the object element, and its value is not application/octet-stream,
// then run the following steps:
else if (auto type = this->type(); !type.is_empty() && (type != "application/octet-stream"sv)) {
// 1. If the attribute's value is a type that starts with "image/" that is not also an XML MIME type, then
// let the resource type be the type specified in that type attribute.
if (type.starts_with_bytes("image/"sv)) {
auto parsed_type = MimeSniff::MimeType::parse(type);
if (parsed_type.has_value() && !parsed_type->is_xml())
resource_type = move(parsed_type);
}
// 2. Jump to the step below labeled handler.
}
}
// -> Otherwise, if the resource does not have associated Content-Type metadata
else {
Optional<MimeSniff::MimeType> tentative_type;
// 1. If there is a type attribute present on the object element, then let the tentative type be the type specified in that type attribute.
// Otherwise, let tentative type be the computed type of the resource.
if (auto type = this->type(); !type.is_empty())
tentative_type = MimeSniff::MimeType::parse(type);
else
tentative_type = MimeSniff::Resource::sniff(data);
// 2. If tentative type is not application/octet-stream, then let resource type be tentative type and jump to the
// step below labeled handler.
if (tentative_type.has_value() && tentative_type->essence() != "application/octet-stream"sv)
resource_type = move(tentative_type);
}
if (resource_type.has_value())
run_object_representation_handler_steps(response, *resource_type, data);
else
run_object_representation_fallback_steps();
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element:plugin-11
void HTMLObjectElement::run_object_representation_handler_steps(Fetch::Infrastructure::Response const& response, MimeSniff::MimeType const& resource_type, ReadonlyBytes data)
{
// 3.9. Handler: Handle the content as given by the first of the following cases that matches:
// -> If the resource type is an XML MIME type, or if the resource type does not start with "image/"
if (can_load_document_with_type(resource_type) && (resource_type.is_xml() || !resource_type.is_image())) {
// If the object element's content navigable is null, then create a new child navigable for the element.
if (!m_content_navigable && in_a_document_tree()) {
MUST(create_new_child_navigable());
set_content_navigable_initialized();
}
// NOTE: Creating a new nested browsing context can fail if the document is not attached to a browsing context
if (!m_content_navigable)
return;
// Let response be the response from fetch.
// If response's URL does not match about:blank, then navigate the element's content navigable to response's URL
// using the element's node document, with historyHandling set to "replace".
if (response.url().has_value() && !url_matches_about_blank(*response.url())) {
MUST(m_content_navigable->navigate({
.url = *response.url(),
.source_document = document(),
.history_handling = Bindings::NavigationHistoryBehavior::Replace,
}));
}
// The object element represents its content navigable.
run_object_representation_completed_steps(Representation::ContentNavigable);
}
// -> If the resource type starts with "image/", and support for images has not been disabled
// FIXME: Handle disabling image support.
else if (resource_type.is_image()) {
// Destroy a child navigable given the object element.
destroy_the_child_navigable();
// Apply the image sniffing rules to determine the type of the image.
// The object element represents the specified image.
// If the image cannot be rendered, e.g. because it is malformed or in an unsupported format, jump to the step
// below labeled fallback.
if (data.is_empty()) {
run_object_representation_fallback_steps();
return;
}
load_image();
}
// -> Otherwise
else {
// The given resource type is not supported. Jump to the step below labeled fallback.
run_object_representation_fallback_steps();
}
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element:the-object-element-19
void HTMLObjectElement::run_object_representation_completed_steps(Representation representation)
{
// 3.10. The element's contents are not part of what the object element represents.
// 3.11. If the object element does not represent its content navigable, then once the resource is completely loaded,
// queue an element task on the DOM manipulation task source given the object element to fire an event named
// load at the element.
if (representation != Representation::ContentNavigable) {
queue_an_element_task(HTML::Task::Source::DOMManipulation, [&]() {
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load));
});
}
update_layout_and_child_objects(representation);
// 3.12. Return.
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element:the-object-element-23
void HTMLObjectElement::run_object_representation_fallback_steps()
{
// 4. Fallback: The object element represents the element's children. This is the element's fallback content.
// Destroy a child navigable given the element.
destroy_the_child_navigable();
update_layout_and_child_objects(Representation::Children);
}
void HTMLObjectElement::load_image()
{
// FIXME: This currently reloads the image instead of reusing the resource we've already downloaded.
auto data = get_attribute_value(HTML::AttributeNames::data);
auto url = document().encoding_parse_url(data);
m_resource_request = HTML::SharedResourceRequest::get_or_create(realm(), document().page(), url);
m_resource_request->add_callbacks(
[this] {
run_object_representation_completed_steps(Representation::Image);
},
[this] {
run_object_representation_fallback_steps();
});
if (m_resource_request->needs_fetching()) {
auto request = HTML::create_potential_CORS_request(vm(), url, Fetch::Infrastructure::Request::Destination::Image, HTML::CORSSettingAttribute::NoCORS);
request->set_client(&document().relevant_settings_object());
m_resource_request->fetch_resource(realm(), request);
}
}
void HTMLObjectElement::update_layout_and_child_objects(Representation representation)
{
if (representation == Representation::Children) {
for_each_child_of_type<HTMLObjectElement>([](auto& object) {
object.queue_element_task_to_run_object_representation_steps();
return IterationDecision::Continue;
});
}
m_representation = representation;
invalidate_style(DOM::StyleInvalidationReason::HTMLObjectElementUpdateLayoutAndChildObjects);
document().invalidate_layout_tree();
}
// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
i32 HTMLObjectElement::default_tab_index_value() const
{
// See the base function for the spec comments.
return 0;
}
GC::Ptr<DecodedImageData> HTMLObjectElement::image_data() const
{
if (!m_resource_request)
return nullptr;
return m_resource_request->image_data();
}
bool HTMLObjectElement::is_image_available() const
{
return image_data() != nullptr;
}
Optional<CSSPixels> HTMLObjectElement::intrinsic_width() const
{
if (auto image_data = this->image_data())
return image_data->intrinsic_width();
return {};
}
Optional<CSSPixels> HTMLObjectElement::intrinsic_height() const
{
if (auto image_data = this->image_data())
return image_data->intrinsic_height();
return {};
}
Optional<CSSPixelFraction> HTMLObjectElement::intrinsic_aspect_ratio() const
{
if (auto image_data = this->image_data())
return image_data->intrinsic_aspect_ratio();
return {};
}
RefPtr<Gfx::ImmutableBitmap> HTMLObjectElement::current_image_bitmap(Gfx::IntSize size) const
{
if (auto image_data = this->image_data())
return image_data->bitmap(0, size);
return nullptr;
}
void HTMLObjectElement::set_visible_in_viewport(bool)
{
// FIXME: Loosen grip on image data when it's not visible, e.g via volatile memory.
}
}