Coverage Report

Created: 2025-03-04 07:22

/src/serenity/Userland/Libraries/LibWeb/CSS/FontFace.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <LibCore/Promise.h>
8
#include <LibGfx/Font/OpenType/Font.h>
9
#include <LibGfx/Font/VectorFont.h>
10
#include <LibGfx/Font/WOFF/Font.h>
11
#include <LibGfx/Font/WOFF2/Font.h>
12
#include <LibJS/Heap/Heap.h>
13
#include <LibJS/Runtime/ArrayBuffer.h>
14
#include <LibJS/Runtime/Realm.h>
15
#include <LibWeb/Bindings/FontFacePrototype.h>
16
#include <LibWeb/Bindings/Intrinsics.h>
17
#include <LibWeb/CSS/FontFace.h>
18
#include <LibWeb/CSS/Parser/Parser.h>
19
#include <LibWeb/CSS/StyleComputer.h>
20
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
21
#include <LibWeb/HTML/Window.h>
22
#include <LibWeb/Platform/EventLoopPlugin.h>
23
#include <LibWeb/WebIDL/AbstractOperations.h>
24
#include <LibWeb/WebIDL/Buffers.h>
25
#include <LibWeb/WebIDL/Promise.h>
26
27
namespace Web::CSS {
28
29
static NonnullRefPtr<Core::Promise<NonnullRefPtr<Gfx::VectorFont>>> load_vector_font(ByteBuffer const& data)
30
0
{
31
0
    auto promise = Core::Promise<NonnullRefPtr<Gfx::VectorFont>>::construct();
32
33
    // FIXME: 'Asynchronously' shouldn't mean 'later on the main thread'.
34
    //        Can we defer this to a background thread?
35
0
    Platform::EventLoopPlugin::the().deferred_invoke([&data, promise] {
36
        // FIXME: This should be de-duplicated with StyleComputer::FontLoader::try_load_font
37
        // We don't have the luxury of knowing the MIME type, so we have to try all formats.
38
0
        auto ttf = OpenType::Font::try_load_from_externally_owned_memory(data);
39
0
        if (!ttf.is_error()) {
40
0
            promise->resolve(ttf.release_value());
41
0
            return;
42
0
        }
43
0
        auto woff = WOFF::Font::try_load_from_externally_owned_memory(data);
44
0
        if (!woff.is_error()) {
45
0
            promise->resolve(woff.release_value());
46
0
            return;
47
0
        }
48
0
        auto woff2 = WOFF2::Font::try_load_from_externally_owned_memory(data);
49
0
        if (!woff2.is_error()) {
50
0
            promise->resolve(woff2.release_value());
51
0
            return;
52
0
        }
53
0
        promise->reject(Error::from_string_literal("Automatic format detection failed"));
54
0
    });
55
56
0
    return promise;
57
0
}
58
59
JS_DEFINE_ALLOCATOR(FontFace);
60
61
template<CSS::PropertyID PropertyID>
62
RefPtr<CSSStyleValue const> parse_property_string(JS::Realm& realm, StringView value)
63
0
{
64
0
    auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), value);
65
0
    return parser.parse_as_css_value(PropertyID);
66
0
}
Unexecuted instantiation: AK::RefPtr<Web::CSS::CSSStyleValue const> Web::CSS::parse_property_string<(Web::CSS::PropertyID)54>(JS::Realm&, AK::StringView)
Unexecuted instantiation: AK::RefPtr<Web::CSS::CSSStyleValue const> Web::CSS::parse_property_string<(Web::CSS::PropertyID)58>(JS::Realm&, AK::StringView)
Unexecuted instantiation: AK::RefPtr<Web::CSS::CSSStyleValue const> Web::CSS::parse_property_string<(Web::CSS::PropertyID)61>(JS::Realm&, AK::StringView)
Unexecuted instantiation: AK::RefPtr<Web::CSS::CSSStyleValue const> Web::CSS::parse_property_string<(Web::CSS::PropertyID)62>(JS::Realm&, AK::StringView)
67
68
// https://drafts.csswg.org/css-font-loading/#font-face-constructor
69
JS::NonnullGCPtr<FontFace> FontFace::construct_impl(JS::Realm& realm, String family, FontFaceSource source, FontFaceDescriptors const& descriptors)
70
0
{
71
0
    auto& vm = realm.vm();
72
0
    auto base_url = HTML::relevant_settings_object(realm.global_object()).api_base_url();
73
74
    // 1. Let font face be a fresh FontFace object. Set font face’s status attribute to "unloaded",
75
    //    Set its internal [[FontStatusPromise]] slot to a fresh pending Promise object.
76
0
    auto promise = WebIDL::create_promise(realm);
77
78
    // FIXME: Parse the family argument, and the members of the descriptors argument,
79
    //    according to the grammars of the corresponding descriptors of the CSS @font-face rule.
80
    //    If the source argument is a CSSOMString, parse it according to the grammar of the CSS src descriptor of the @font-face rule.
81
    //    If any of them fail to parse correctly, reject font face’s [[FontStatusPromise]] with a DOMException named "SyntaxError",
82
    //    set font face’s corresponding attributes to the empty string, and set font face’s status attribute to "error".
83
    //    Otherwise, set font face’s corresponding attributes to the serialization of the parsed values.
84
85
    // 2. (Out of order) If the source argument was a CSSOMString, set font face’s internal [[Urls]]
86
    //    slot to the string.
87
    //    If the source argument was a BinaryData, set font face’s internal [[Data]] slot
88
    //    to the passed argument.
89
0
    Vector<CSS::ParsedFontFace::Source> sources;
90
0
    ByteBuffer buffer;
91
0
    if (auto* string = source.get_pointer<String>()) {
92
0
        auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm, base_url), *string);
93
0
        sources = parser.parse_as_font_face_src();
94
0
        if (sources.is_empty())
95
0
            WebIDL::reject_promise(realm, promise, WebIDL::SyntaxError::create(realm, "FontFace constructor: Invalid source string"_string));
96
0
    } else {
97
0
        auto buffer_source = source.get<JS::Handle<WebIDL::BufferSource>>();
98
0
        auto maybe_buffer = WebIDL::get_buffer_source_copy(buffer_source->raw_object());
99
0
        if (maybe_buffer.is_error()) {
100
0
            VERIFY(maybe_buffer.error().code() == ENOMEM);
101
0
            auto throw_completion = vm.throw_completion<JS::InternalError>(vm.error_message(JS::VM::ErrorMessage::OutOfMemory));
102
0
            WebIDL::reject_promise(realm, promise, *throw_completion.value());
103
0
        } else {
104
0
            buffer = maybe_buffer.release_value();
105
0
        }
106
0
    }
107
108
0
    if (buffer.is_empty() && sources.is_empty())
109
0
        WebIDL::reject_promise(realm, promise, WebIDL::SyntaxError::create(realm, "FontFace constructor: Invalid font source"_string));
110
111
0
    auto font = realm.heap().allocate<FontFace>(realm, realm, promise, move(sources), move(buffer), move(family), descriptors);
112
113
    // 1. (continued) Return font face. If font face’s status is "error", terminate this algorithm;
114
    //    otherwise, complete the rest of these steps asynchronously.
115
0
    if (font->status() == Bindings::FontFaceLoadStatus::Error)
116
0
        return font;
117
118
    // 3. If font face’s [[Data]] slot is not null, queue a task to run the following steps synchronously:
119
0
    if (font->m_binary_data.is_empty())
120
0
        return font;
121
122
0
    HTML::queue_global_task(HTML::Task::Source::FontLoading, HTML::relevant_global_object(*font), JS::create_heap_function(vm.heap(), [font] {
123
        // 1.  Set font face’s status attribute to "loading".
124
0
        font->m_status = Bindings::FontFaceLoadStatus::Loading;
125
126
        // 2. FIXME: For each FontFaceSet font face is in:
127
128
        // 3. Asynchronously, attempt to parse the data in it as a font.
129
        //    When this is completed, successfully or not, queue a task to run the following steps synchronously:
130
0
        font->m_font_load_promise = load_vector_font(font->m_binary_data);
131
132
0
        font->m_font_load_promise->when_resolved([font = JS::make_handle(font)](auto const& vector_font) -> ErrorOr<void> {
133
0
            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] {
134
0
                HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*font), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
135
                // 1. If the load was successful, font face now represents the parsed font;
136
                //    fulfill font face’s [[FontStatusPromise]] with font face, and set its status attribute to "loaded".
137
138
                // FIXME: Are we supposed to set the properties of the FontFace based on the loaded vector font?
139
0
                font->m_parsed_font = vector_font;
140
0
                font->m_status = Bindings::FontFaceLoadStatus::Loaded;
141
0
                WebIDL::resolve_promise(font->realm(), font->m_font_status_promise, font);
142
143
                // FIXME: For each FontFaceSet font face is in:
144
145
0
                font->m_font_load_promise = nullptr;
146
0
            }));
147
0
            return {};
148
0
        });
149
0
        font->m_font_load_promise->when_rejected([font = JS::make_handle(font)](auto const& error) {
150
0
            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)] {
151
0
                HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*font), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
152
                // 2. Otherwise, reject font face’s [[FontStatusPromise]] with a DOMException named "SyntaxError"
153
                //    and set font face’s status attribute to "error".
154
0
                font->m_status = Bindings::FontFaceLoadStatus::Error;
155
0
                WebIDL::reject_promise(font->realm(), font->m_font_status_promise, WebIDL::SyntaxError::create(font->realm(), MUST(String::formatted("Failed to load font: {}", error))));
156
157
                // FIXME: For each FontFaceSet font face is in:
158
159
0
                font->m_font_load_promise = nullptr;
160
0
            }));
161
0
        });
162
0
    }));
163
164
0
    return font;
165
0
}
166
167
FontFace::FontFace(JS::Realm& realm, JS::NonnullGCPtr<WebIDL::Promise> font_status_promise, Vector<ParsedFontFace::Source> urls, ByteBuffer data, String font_family, FontFaceDescriptors const& descriptors)
168
0
    : Bindings::PlatformObject(realm)
169
0
    , m_font_status_promise(font_status_promise)
170
0
    , m_urls(move(urls))
171
0
    , m_binary_data(move(data))
172
0
{
173
0
    m_family = move(font_family);
174
0
    m_style = descriptors.style;
175
0
    m_weight = descriptors.weight;
176
0
    m_stretch = descriptors.stretch;
177
0
    m_unicode_range = descriptors.unicode_range;
178
0
    m_feature_settings = descriptors.feature_settings;
179
0
    m_variation_settings = descriptors.variation_settings;
180
0
    m_display = descriptors.display;
181
0
    m_ascent_override = descriptors.ascent_override;
182
0
    m_descent_override = descriptors.descent_override;
183
0
    m_line_gap_override = descriptors.line_gap_override;
184
185
    // FIXME: Parse from descriptor
186
    // FIXME: Have gettter reflect this member instead of the string
187
0
    m_unicode_ranges.empend(0x0u, 0x10FFFFu);
188
189
0
    if (verify_cast<JS::Promise>(*m_font_status_promise->promise()).state() == JS::Promise::State::Rejected)
190
0
        m_status = Bindings::FontFaceLoadStatus::Error;
191
0
}
192
193
0
FontFace::~FontFace() = default;
194
195
void FontFace::initialize(JS::Realm& realm)
196
0
{
197
0
    Base::initialize(realm);
198
199
0
    WEB_SET_PROTOTYPE_FOR_INTERFACE(FontFace);
200
0
}
201
202
void FontFace::visit_edges(JS::Cell::Visitor& visitor)
203
0
{
204
0
    Base::visit_edges(visitor);
205
206
0
    visitor.visit(m_font_status_promise);
207
0
}
208
209
JS::NonnullGCPtr<JS::Promise> FontFace::loaded() const
210
0
{
211
0
    return verify_cast<JS::Promise>(*m_font_status_promise->promise());
212
0
}
213
214
// https://drafts.csswg.org/css-font-loading/#dom-fontface-family
215
WebIDL::ExceptionOr<void> FontFace::set_family(String const& string)
216
0
{
217
0
    auto property = parse_property_string<CSS::PropertyID::FontFamily>(realm(), string);
218
0
    if (!property)
219
0
        return WebIDL::SyntaxError::create(realm(), "FontFace.family setter: Invalid font descriptor"_string);
220
221
0
    if (m_is_css_connected) {
222
        // FIXME: Propagate to the CSSFontFaceRule and update the font-family property
223
0
    }
224
225
0
    m_family = property->to_string();
226
227
0
    return {};
228
0
}
229
230
// https://drafts.csswg.org/css-font-loading/#dom-fontface-style
231
WebIDL::ExceptionOr<void> FontFace::set_style(String const& string)
232
0
{
233
0
    auto property = parse_property_string<CSS::PropertyID::FontStyle>(realm(), string);
234
0
    if (!property)
235
0
        return WebIDL::SyntaxError::create(realm(), "FontFace.style setter: Invalid font descriptor"_string);
236
237
0
    if (m_is_css_connected) {
238
        // FIXME: Propagate to the CSSFontFaceRule and update the font-style property
239
0
    }
240
241
0
    m_style = property->to_string();
242
243
0
    return {};
244
0
}
245
246
// https://drafts.csswg.org/css-font-loading/#dom-fontface-weight
247
WebIDL::ExceptionOr<void> FontFace::set_weight(String const& string)
248
0
{
249
0
    auto property = parse_property_string<CSS::PropertyID::FontWeight>(realm(), string);
250
0
    if (!property)
251
0
        return WebIDL::SyntaxError::create(realm(), "FontFace.weight setter: Invalid font descriptor"_string);
252
253
0
    if (m_is_css_connected) {
254
        // FIXME: Propagate to the CSSFontFaceRule and update the font-weight property
255
0
    }
256
257
0
    m_weight = property->to_string();
258
259
0
    return {};
260
0
}
261
262
// https://drafts.csswg.org/css-font-loading/#dom-fontface-stretch
263
WebIDL::ExceptionOr<void> FontFace::set_stretch(String const& string)
264
0
{
265
    // NOTE: font-stretch is now an alias for font-width
266
0
    auto property = parse_property_string<CSS::PropertyID::FontWidth>(realm(), string);
267
0
    if (!property)
268
0
        return WebIDL::SyntaxError::create(realm(), "FontFace.stretch setter: Invalid font descriptor"_string);
269
270
0
    if (m_is_css_connected) {
271
        // FIXME: Propagate to the CSSFontFaceRule and update the font-width property
272
0
    }
273
274
0
    m_stretch = property->to_string();
275
276
0
    return {};
277
0
}
278
279
// https://drafts.csswg.org/css-font-loading/#dom-fontface-unicoderange
280
WebIDL::ExceptionOr<void> FontFace::set_unicode_range(String const&)
281
0
{
282
    // FIXME: This *should* work, but the <urange> production is hard to parse
283
    //        from just a value string in our implementation
284
0
    return WebIDL::NotSupportedError::create(realm(), "unicode range is not yet implemented"_string);
285
0
}
286
287
// https://drafts.csswg.org/css-font-loading/#dom-fontface-featuresettings
288
WebIDL::ExceptionOr<void> FontFace::set_feature_settings(String const&)
289
0
{
290
0
    return WebIDL::NotSupportedError::create(realm(), "feature settings is not yet implemented"_string);
291
0
}
292
293
// https://drafts.csswg.org/css-font-loading/#dom-fontface-variationsettings
294
WebIDL::ExceptionOr<void> FontFace::set_variation_settings(String const&)
295
0
{
296
0
    return WebIDL::NotSupportedError::create(realm(), "variation settings is not yet implemented"_string);
297
0
}
298
299
// https://drafts.csswg.org/css-font-loading/#dom-fontface-display
300
WebIDL::ExceptionOr<void> FontFace::set_display(String const&)
301
0
{
302
0
    return WebIDL::NotSupportedError::create(realm(), "display is not yet implemented"_string);
303
0
}
304
305
// https://drafts.csswg.org/css-font-loading/#dom-fontface-ascentoverride
306
WebIDL::ExceptionOr<void> FontFace::set_ascent_override(String const&)
307
0
{
308
0
    return WebIDL::NotSupportedError::create(realm(), "ascent override is not yet implemented"_string);
309
0
}
310
311
// https://drafts.csswg.org/css-font-loading/#dom-fontface-descentoverride
312
WebIDL::ExceptionOr<void> FontFace::set_descent_override(String const&)
313
0
{
314
0
    return WebIDL::NotSupportedError::create(realm(), "descent override is not yet implemented"_string);
315
0
}
316
317
// https://drafts.csswg.org/css-font-loading/#dom-fontface-linegapoverride
318
WebIDL::ExceptionOr<void> FontFace::set_line_gap_override(String const&)
319
0
{
320
0
    return WebIDL::NotSupportedError::create(realm(), "line gap override is not yet implemented"_string);
321
0
}
322
323
// https://drafts.csswg.org/css-font-loading/#dom-fontface-load
324
JS::NonnullGCPtr<JS::Promise> FontFace::load()
325
0
{
326
    //  1. Let font face be the FontFace object on which this method was called.
327
0
    auto& font_face = *this;
328
329
    // 2. If font face’s [[Urls]] slot is null, or its status attribute is anything other than "unloaded",
330
    //    return font face’s [[FontStatusPromise]] and abort these steps.
331
0
    if (font_face.m_urls.is_empty() || font_face.m_status != Bindings::FontFaceLoadStatus::Unloaded)
332
0
        return font_face.loaded();
333
334
0
    load_font_source();
335
336
0
    return font_face.loaded();
337
0
}
338
339
void FontFace::load_font_source()
340
0
{
341
0
    VERIFY(!m_urls.is_empty() && m_status == Bindings::FontFaceLoadStatus::Unloaded);
342
    // NOTE: These steps are from the load() method, but can also be called by the user agent when the font
343
    //       is needed to render something on the page.
344
345
    // User agents can initiate font loads on their own, whenever they determine that a given font face is necessary
346
    // to render something on the page. When this happens, they must act as if they had called the corresponding
347
    // FontFace’s load() method described here.
348
349
    // 3. Otherwise, set font face’s status attribute to "loading", return font face’s [[FontStatusPromise]],
350
    //    and continue executing the rest of this algorithm asynchronously.
351
0
    m_status = Bindings::FontFaceLoadStatus::Loading;
352
353
0
    Web::Platform::EventLoopPlugin::the().deferred_invoke([font = JS::make_handle(this)] {
354
        // 4. Using the value of font face’s [[Urls]] slot, attempt to load a font as defined in [CSS-FONTS-3],
355
        //     as if it was the value of a @font-face rule’s src descriptor.
356
357
        // 5. When the load operation completes, successfully or not, queue a task to run the following steps synchronously:
358
0
        auto on_error = [font] {
359
0
            HTML::queue_global_task(HTML::Task::Source::FontLoading, HTML::relevant_global_object(*font), JS::create_heap_function(font->heap(), [font = JS::NonnullGCPtr(*font)] {
360
0
                HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*font), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
361
362
                //     1. If the attempt to load fails, reject font face’s [[FontStatusPromise]] with a DOMException whose name
363
                //        is "NetworkError" and set font face’s status attribute to "error".
364
0
                font->m_status = Bindings::FontFaceLoadStatus::Error;
365
0
                WebIDL::reject_promise(font->realm(), font->m_font_status_promise, WebIDL::NetworkError::create(font->realm(), "Failed to load font"_string));
366
367
                // FIXME: For each FontFaceSet font face is in:
368
0
            }));
369
0
        };
370
371
0
        auto on_load = [font](FontLoader const& loader) {
372
            // FIXME: We are assuming that the font loader will live as long as the document! This is an unsafe capture
373
0
            HTML::queue_global_task(HTML::Task::Source::FontLoading, HTML::relevant_global_object(*font), JS::create_heap_function(font->heap(), [font = JS::NonnullGCPtr(*font), &loader] {
374
0
                HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*font), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
375
376
                // 2. Otherwise, font face now represents the loaded font; fulfill font face’s [[FontStatusPromise]] with font face
377
                //    and set font face’s status attribute to "loaded".
378
0
                font->m_parsed_font = loader.vector_font();
379
0
                font->m_status = Bindings::FontFaceLoadStatus::Loaded;
380
0
                WebIDL::resolve_promise(font->realm(), font->m_font_status_promise, font);
381
382
                // FIXME: For each FontFaceSet font face is in:
383
0
            }));
384
0
        };
385
386
        // FIXME: We should probably put the 'font cache' on the WindowOrWorkerGlobalScope instead of tying it to the document's style computer
387
0
        auto& global = HTML::relevant_global_object(*font);
388
0
        if (is<HTML::Window>(global)) {
389
0
            auto& window = static_cast<HTML::Window&>(global);
390
0
            auto& style_computer = const_cast<StyleComputer&>(window.document()->style_computer());
391
392
            // FIXME: The ParsedFontFace is kind of expensive to create. We should be using a shared sub-object for the data
393
0
            ParsedFontFace parsed_font_face {
394
0
                font->m_family,
395
0
                font->m_weight.to_number<int>(),
396
0
                0,                      // FIXME: slope
397
0
                Gfx::FontWidth::Normal, // FIXME: width
398
0
                font->m_urls,
399
0
                font->m_unicode_ranges,
400
0
                {},                // FIXME: ascent_override
401
0
                {},                // FIXME: descent_override
402
0
                {},                // FIXME: line_gap_override
403
0
                FontDisplay::Auto, // FIXME: font_display
404
0
                {},                // font-named-instance doesn't exist in FontFace
405
0
                {},                // font-language-override doesn't exist in FontFace
406
0
                {},                // FIXME: feature_settings
407
0
                {},                // FIXME: variation_settings
408
0
            };
409
0
            if (auto loader = style_computer.load_font_face(parsed_font_face, move(on_load), move(on_error)); loader.has_value())
410
0
                loader->start_loading_next_url();
411
0
        } else {
412
            // FIXME: Don't know how to load fonts in workers! They don't have a StyleComputer
413
0
            dbgln("FIXME: Worker font loading not implemented");
414
0
        }
415
0
    });
416
0
}
417
418
}