diff --git a/Tests/LibWeb/Text/expected/css/FontFace-binary-data.txt b/Tests/LibWeb/Text/expected/css/FontFace-binary-data.txt new file mode 100644 index 00000000000..9c5f67d257e --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/FontFace-binary-data.txt @@ -0,0 +1,6 @@ +badFace.status: error +badFace.family: My Cool Font +zeroBytesFace.status: error +zeroBytesFace.family: Empty Font +face.status: loaded +face.family: Hash Sans diff --git a/Tests/LibWeb/Text/input/css/FontFace-binary-data.html b/Tests/LibWeb/Text/input/css/FontFace-binary-data.html new file mode 100644 index 00000000000..973296b1aab --- /dev/null +++ b/Tests/LibWeb/Text/input/css/FontFace-binary-data.html @@ -0,0 +1,42 @@ + + + diff --git a/Userland/Libraries/LibWeb/CSS/FontFace.cpp b/Userland/Libraries/LibWeb/CSS/FontFace.cpp index 5a6b55ed8b9..8e7ccd63ae3 100644 --- a/Userland/Libraries/LibWeb/CSS/FontFace.cpp +++ b/Userland/Libraries/LibWeb/CSS/FontFace.cpp @@ -4,16 +4,56 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include +#include +#include +#include #include +#include #include #include #include #include #include +#include +#include +#include +#include #include namespace Web::CSS { +static NonnullRefPtr>> load_vector_font(ByteBuffer const& data) +{ + auto promise = Core::Promise>::construct(); + + // FIXME: 'Asynchronously' shouldn't mean 'later on the main thread'. + // Can we defer this to a background thread? + Platform::EventLoopPlugin::the().deferred_invoke([&data, promise] { + // FIXME: This should be de-duplicated with StyleComputer::FontLoader::try_load_font + // We don't have the luxury of knowing the MIME type, so we have to try all formats. + auto ttf = OpenType::Font::try_load_from_externally_owned_memory(data); + if (!ttf.is_error()) { + promise->resolve(ttf.release_value()); + return; + } + auto woff = WOFF::Font::try_load_from_externally_owned_memory(data); + if (!woff.is_error()) { + promise->resolve(woff.release_value()); + return; + } + auto woff2 = WOFF2::Font::try_load_from_externally_owned_memory(data); + if (!woff2.is_error()) { + promise->resolve(woff2.release_value()); + return; + } + promise->reject(Error::from_string_literal("Automatic format detection failed")); + }); + + return promise; +} + JS_DEFINE_ALLOCATOR(FontFace); template @@ -26,15 +66,115 @@ RefPtr parse_property_string(JS::Realm& realm, StringView return maybe_parser.release_value().parse_as_css_value(PropertyID); } +// https://drafts.csswg.org/css-font-loading/#font-face-constructor JS::NonnullGCPtr FontFace::construct_impl(JS::Realm& realm, String family, FontFaceSource source, FontFaceDescriptors const& descriptors) { - return realm.heap().allocate(realm, realm, move(family), move(source), descriptors); + auto& vm = realm.vm(); + + // 1. Let font face be a fresh FontFace object. Set font face’s status attribute to "unloaded", + // Set its internal [[FontStatusPromise]] slot to a fresh pending Promise object. + auto promise = WebIDL::create_promise(realm); + + // FIXME: Parse the family argument, and the members of the descriptors argument, + // according to the grammars of the corresponding descriptors of the CSS @font-face rule. + // If the source argument is a CSSOMString, parse it according to the grammar of the CSS src descriptor of the @font-face rule. + // If any of them fail to parse correctly, reject font face’s [[FontStatusPromise]] with a DOMException named "SyntaxError", + // set font face’s corresponding attributes to the empty string, and set font face’s status attribute to "error". + // Otherwise, set font face’s corresponding attributes to the serialization of the parsed values. + + // 2. (Out of order) If the source argument was a CSSOMString, set font face’s internal [[Urls]] + // slot to the string. + // If the source argument was a BinaryData, set font face’s internal [[Data]] slot + // to the passed argument. + Vector sources; + ByteBuffer buffer; + if (auto* string = source.get_pointer()) { + auto maybe_parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), *string); + if (maybe_parser.is_error()) { + WebIDL::reject_promise(realm, promise, WebIDL::SyntaxError::create(realm, "FontFace constructor: Invalid source string"_fly_string)); + } else { + auto parser = maybe_parser.release_value(); + sources = parser.parse_as_font_face_src(); + if (sources.is_empty()) + WebIDL::reject_promise(realm, promise, WebIDL::SyntaxError::create(realm, "FontFace constructor: Invalid source string"_fly_string)); + } + } else { + auto buffer_source = source.get>(); + auto maybe_buffer = WebIDL::get_buffer_source_copy(buffer_source->raw_object()); + if (maybe_buffer.is_error()) { + VERIFY(maybe_buffer.error().code() == ENOMEM); + auto throw_completion = vm.throw_completion(vm.error_message(JS::VM::ErrorMessage::OutOfMemory)); + WebIDL::reject_promise(realm, promise, *throw_completion.value()); + } else { + buffer = maybe_buffer.release_value(); + } + } + + if (buffer.is_empty() && sources.is_empty()) + WebIDL::reject_promise(realm, promise, WebIDL::SyntaxError::create(realm, "FontFace constructor: Invalid font source"_fly_string)); + + auto font = realm.heap().allocate(realm, realm, promise, move(sources), move(buffer), move(family), descriptors); + + // 1. (continued) Return font face. If font face’s status is "error", terminate this algorithm; + // otherwise, complete the rest of these steps asynchronously. + if (font->status() == Bindings::FontFaceLoadStatus::Error) + return font; + + // 3. If font face’s [[Data]] slot is not null, queue a task to run the following steps synchronously: + if (font->m_binary_data.is_empty()) + return font; + + HTML::queue_global_task(HTML::Task::Source::FontLoading, HTML::relevant_global_object(*font), JS::create_heap_function(vm.heap(), [font] { + // 1. Set font face’s status attribute to "loading". + font->m_status = Bindings::FontFaceLoadStatus::Loading; + + // 2. FIXME: For each FontFaceSet font face is in: + + // 3. Asynchronously, attempt to parse the data in it as a font. + // When this is completed, successfully or not, queue a task to run the following steps synchronously: + font->m_font_load_promise = load_vector_font(font->m_binary_data); + + font->m_font_load_promise->when_resolved([font = JS::make_handle(font)](auto const& vector_font) -> ErrorOr { + HTML::queue_global_task(HTML::Task::Source::FontLoading, HTML::relevant_global_object(*font), JS::create_heap_function(font->heap(), [font = JS::NonnullGCPtr(*font), vector_font] { + HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*font), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // 1. If the load was successful, font face now represents the parsed font; + // fulfill font face’s [[FontStatusPromise]] with font face, and set its status attribute to "loaded". + + // FIXME: Are we supposed to set the properties of the FontFace based on the loaded vector font? + font->m_parsed_font = vector_font; + font->m_status = Bindings::FontFaceLoadStatus::Loaded; + WebIDL::resolve_promise(font->realm(), font->m_font_status_promise, font); + + // FIXME: For each FontFaceSet font face is in: + + font->m_font_load_promise = nullptr; + })); + return {}; + }); + font->m_font_load_promise->when_rejected([font = JS::make_handle(font)](auto const& error) { + HTML::queue_global_task(HTML::Task::Source::FontLoading, HTML::relevant_global_object(*font), JS::create_heap_function(font->heap(), [font = JS::NonnullGCPtr(*font), error = Error::copy(error)] { + HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*font), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // 2. Otherwise, reject font face’s [[FontStatusPromise]] with a DOMException named "SyntaxError" + // and set font face’s status attribute to "error". + font->m_status = Bindings::FontFaceLoadStatus::Error; + WebIDL::reject_promise(font->realm(), font->m_font_status_promise, WebIDL::SyntaxError::create(font->realm(), MUST(String::formatted("Failed to load font: {}", error)))); + + // FIXME: For each FontFaceSet font face is in: + + font->m_font_load_promise = nullptr; + })); + }); + })); + + return font; } -FontFace::FontFace(JS::Realm& realm, String font_family, FontFaceSource, FontFaceDescriptors const& descriptors) +FontFace::FontFace(JS::Realm& realm, JS::NonnullGCPtr font_status_promise, Vector urls, ByteBuffer data, String font_family, FontFaceDescriptors const& descriptors) : Bindings::PlatformObject(realm) + , m_font_status_promise(font_status_promise) + , m_urls(move(urls)) + , m_binary_data(move(data)) { - // FIXME: Validate these values the same way as the setters m_family = move(font_family); m_style = descriptors.style; m_weight = descriptors.weight; @@ -46,6 +186,9 @@ FontFace::FontFace(JS::Realm& realm, String font_family, FontFaceSource, FontFac m_ascent_override = descriptors.ascent_override; m_descent_override = descriptors.descent_override; m_line_gap_override = descriptors.line_gap_override; + + if (verify_cast(*m_font_status_promise->promise()).state() == JS::Promise::State::Rejected) + m_status = Bindings::FontFaceLoadStatus::Error; } void FontFace::initialize(JS::Realm& realm) @@ -55,6 +198,18 @@ void FontFace::initialize(JS::Realm& realm) WEB_SET_PROTOTYPE_FOR_INTERFACE(FontFace); } +void FontFace::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + + visitor.visit(m_font_status_promise); +} + +JS::NonnullGCPtr FontFace::loaded() const +{ + return verify_cast(*m_font_status_promise->promise()); +} + // https://drafts.csswg.org/css-font-loading/#dom-fontface-family WebIDL::ExceptionOr FontFace::set_family(String const& string) { diff --git a/Userland/Libraries/LibWeb/CSS/FontFace.h b/Userland/Libraries/LibWeb/CSS/FontFace.h index 2206224d45c..c9ac96d73f0 100644 --- a/Userland/Libraries/LibWeb/CSS/FontFace.h +++ b/Userland/Libraries/LibWeb/CSS/FontFace.h @@ -6,7 +6,11 @@ #pragma once +#include +#include +#include #include +#include namespace Web::CSS { @@ -28,7 +32,7 @@ class FontFace final : public Bindings::PlatformObject { JS_DECLARE_ALLOCATOR(FontFace); public: - using FontFaceSource = Variant, JS::Handle>; + using FontFaceSource = Variant>; [[nodiscard]] static JS::NonnullGCPtr construct_impl(JS::Realm&, String family, FontFaceSource source, FontFaceDescriptors const& descriptors); virtual ~FontFace() override = default; @@ -66,12 +70,16 @@ public: String line_gap_override() const { return m_line_gap_override; } WebIDL::ExceptionOr set_line_gap_override(String const&); + Bindings::FontFaceLoadStatus status() const { return m_status; } + JS::ThrowCompletionOr> load(); + JS::NonnullGCPtr loaded() const; private: - FontFace(JS::Realm&, String family, FontFaceSource source, FontFaceDescriptors const& descriptors); + FontFace(JS::Realm&, JS::NonnullGCPtr font_status_promise, Vector urls, ByteBuffer data, String family, FontFaceDescriptors const& descriptors); virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Visitor&) override; // FIXME: Should we be storing StyleValues instead? String m_family; @@ -86,6 +94,16 @@ private: String m_descent_override; String m_line_gap_override; + // https://drafts.csswg.org/css-font-loading/#dom-fontface-status + Bindings::FontFaceLoadStatus m_status { Bindings::FontFaceLoadStatus::Unloaded }; + + JS::NonnullGCPtr m_font_status_promise; // [[FontStatusPromise]] + Vector m_urls; // [[Urls]] + ByteBuffer m_binary_data; // [[Data]] + + RefPtr m_parsed_font; + RefPtr>> m_font_load_promise; + // https://drafts.csswg.org/css-font-loading/#css-connected bool m_is_css_connected { false }; }; diff --git a/Userland/Libraries/LibWeb/CSS/FontFace.idl b/Userland/Libraries/LibWeb/CSS/FontFace.idl index b73fc541f66..8eaeae2e816 100644 --- a/Userland/Libraries/LibWeb/CSS/FontFace.idl +++ b/Userland/Libraries/LibWeb/CSS/FontFace.idl @@ -1,5 +1,3 @@ -typedef (ArrayBuffer or ArrayBufferView) BinaryData; - dictionary FontFaceDescriptors { CSSOMString style = "normal"; CSSOMString weight = "normal"; @@ -18,8 +16,8 @@ enum FontFaceLoadStatus { "unloaded", "loading", "loaded", "error" }; // https://drafts.csswg.org/css-font-loading/#fontface-interface [Exposed=(Window,Worker)] interface FontFace { - // FIXME: constructor(CSSOMString family, (CSSOMString or BinaryData) source, optional FontFaceDescriptors descriptors = {}); - constructor(CSSOMString family, CSSOMString source, optional FontFaceDescriptors descriptors = {}); + // FIXME: BufferSource usage needs https://github.com/w3c/csswg-drafts/pull/10309 + constructor(CSSOMString family, (CSSOMString or BufferSource) source, optional FontFaceDescriptors descriptors = {}); attribute CSSOMString family; attribute CSSOMString style; attribute CSSOMString weight; @@ -32,8 +30,8 @@ interface FontFace { attribute CSSOMString descentOverride; attribute CSSOMString lineGapOverride; - // FIXME: readonly attribute FontFaceLoadStatus status; + readonly attribute FontFaceLoadStatus status; Promise load(); - // FIXME: readonly attribute Promise loaded; + readonly attribute Promise loaded; }; diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 0c41eddc704..97518d2674c 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -4566,7 +4566,13 @@ CSSRule* Parser::parse_font_face_rule(TokenStream& tokens) return CSSFontFaceRule::create(m_context.realm(), ParsedFontFace { font_family.release_value(), weight, slope, move(src), move(unicode_range) }); } -Vector Parser::parse_font_face_src(TokenStream& component_values) +Vector Parser::parse_as_font_face_src() +{ + return parse_font_face_src(m_token_stream); +} + +template +Vector Parser::parse_font_face_src(TokenStream& component_values) { // FIXME: Get this information from the system somehow? // Format-name table: https://www.w3.org/TR/css-fonts-4/#font-format-definitions diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index 5e71824103a..dca9c68668b 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -69,6 +69,8 @@ public: Optional parse_as_component_value(); + Vector parse_as_font_face_src(); + static NonnullRefPtr resolve_unresolved_style_value(ParsingContext const&, DOM::Element&, Optional, PropertyID, UnresolvedStyleValue const&); [[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(); @@ -162,7 +164,9 @@ private: Optional parse_general_enclosed(TokenStream&); CSSRule* parse_font_face_rule(TokenStream&); - Vector parse_font_face_src(TokenStream&); + + template + Vector parse_font_face_src(TokenStream&); CSSRule* convert_to_rule(NonnullRefPtr); CSSMediaRule* convert_to_media_rule(NonnullRefPtr); diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h index 58322d19064..756c856cd08 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h @@ -54,6 +54,9 @@ public: // https://w3c.github.io/permissions/#permissions-task-source Permissions, + // https://drafts.csswg.org/css-font-loading/#task-source + FontLoading, + // !!! IMPORTANT: Keep this field last! // This serves as the base value of all unique task sources. // Some elements, such as the HTMLMediaElement, must have a unique task source per instance.