Presenter: Rearchitect on top of LibWeb

This patch replaces the bespoke rendering engine in Presenter with a
simple pipeline that turns presentations into single-page HTML files.
The HTML is then loaded into an OutOfProcessWebView.

This achieves a number of things, most importantly:
- Access to all the CSS features supported by LibWeb
- Sandboxed, multi-process rendering

The code could be simplified a lot further, but I wanted to get the new
architecture in place without changing anything about the file format.
This commit is contained in:
Andreas Kling 2023-01-09 00:44:32 +01:00 committed by Linus Groh
parent ed3c2cbdf6
commit 3110f5b328
Notes: sideshowbarker 2024-07-17 21:16:31 +09:00
10 changed files with 267 additions and 268 deletions

View File

@ -2,7 +2,7 @@ serenity_component(
Presenter
RECOMMENDED
TARGETS Presenter
DEPENDS ImageDecoder FileSystemAccessServer
DEPENDS FileSystemAccessServer
)
@ -14,4 +14,4 @@ set(SOURCES
SlideObject.cpp
)
serenity_app(Presenter ICON app-display-settings)
target_link_libraries(Presenter PRIVATE LibImageDecoderClient LibGUI LibGfx LibFileSystemAccessClient LibCore LibMain)
target_link_libraries(Presenter PRIVATE LibWebView LibGUI LibGfx LibFileSystemAccessClient LibCore LibMain)

View File

@ -1,15 +1,14 @@
/*
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Presentation.h"
#include <AK/Forward.h>
#include <AK/JsonObject.h>
#include <LibCore/Stream.h>
#include <LibGUI/Window.h>
#include <LibGfx/Forward.h>
#include <errno_codes.h>
Presentation::Presentation(Gfx::IntSize normative_size, HashMap<DeprecatedString, DeprecatedString> metadata)
@ -66,7 +65,7 @@ void Presentation::go_to_first_slide()
m_current_slide = 0;
}
ErrorOr<NonnullOwnPtr<Presentation>> Presentation::load_from_file(StringView file_name, NonnullRefPtr<GUI::Window> window)
ErrorOr<NonnullOwnPtr<Presentation>> Presentation::load_from_file(StringView file_name)
{
if (file_name.is_empty())
return ENOENT;
@ -104,7 +103,7 @@ ErrorOr<NonnullOwnPtr<Presentation>> Presentation::load_from_file(StringView fil
return Error::from_string_view("Slides must be objects"sv);
auto const& slide_object = maybe_slide.as_object();
auto slide = TRY(Slide::parse_slide(slide_object, window));
auto slide = TRY(Slide::parse_slide(slide_object));
presentation->append_slide(move(slide));
}
@ -147,15 +146,45 @@ ErrorOr<Gfx::IntSize> Presentation::parse_presentation_size(JsonObject const& me
};
}
void Presentation::paint(Gfx::Painter& painter) const
ErrorOr<DeprecatedString> Presentation::render()
{
auto display_area = painter.clip_rect();
// These two should be the same, but better be safe than sorry.
auto width_scale = static_cast<double>(display_area.width()) / static_cast<double>(m_normative_size.width());
auto height_scale = static_cast<double>(display_area.height()) / static_cast<double>(m_normative_size.height());
auto scale = Gfx::FloatSize { static_cast<float>(width_scale), static_cast<float>(height_scale) };
HTMLElement main_element;
main_element.tag_name = "main"sv;
for (size_t i = 0; i < m_slides.size(); ++i) {
HTMLElement slide_div;
slide_div.tag_name = "div"sv;
TRY(slide_div.style.try_set("display"sv, "none"sv));
TRY(slide_div.attributes.try_set("id"sv, DeprecatedString::formatted("slide{}", i)));
TRY(slide_div.attributes.try_set("class"sv, "slide"));
auto& slide = m_slides[i];
TRY(slide_div.children.try_append(TRY(slide.render(*this))));
main_element.children.append(move(slide_div));
}
// FIXME: Fill the background with a color depending on the color scheme
painter.clear_rect(painter.clip_rect(), Color::White);
current_slide().paint(painter, m_current_frame_in_slide.value(), scale);
StringBuilder builder;
TRY(builder.try_append(R"(
<!DOCTYPE html><html><head><style>
.slide {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
</style><script>
function goto(slideIndex, frameIndex) {
// FIXME: Honor the frameIndex.
let slide;
for (slide of document.getElementsByClassName("slide")) {
slide.style.display = "none";
}
if (slide = document.getElementById(`slide${slideIndex}`))
slide.style.display = "block";
}
window.onload = function() { goto(0, 0) }
</script><body>
)"sv));
TRY(main_element.serialize(builder));
TRY(builder.try_append("</body></html>"sv));
return builder.to_deprecated_string();
}

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -8,11 +9,9 @@
#include "Slide.h"
#include <AK/DeprecatedString.h>
#include <AK/Forward.h>
#include <AK/HashMap.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/Vector.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Size.h>
static constexpr int const PRESENTATION_FORMAT_VERSION = 1;
@ -23,8 +22,7 @@ class Presentation {
public:
~Presentation() = default;
// We can't pass this class directly in an ErrorOr because some of the components are not properly moveable under these conditions.
static ErrorOr<NonnullOwnPtr<Presentation>> load_from_file(StringView file_name, NonnullRefPtr<GUI::Window> window);
static ErrorOr<NonnullOwnPtr<Presentation>> load_from_file(StringView file_name);
StringView title() const;
StringView author() const;
@ -38,8 +36,7 @@ public:
void previous_frame();
void go_to_first_slide();
// This assumes that the caller has clipped the painter to exactly the display area.
void paint(Gfx::Painter& painter) const;
ErrorOr<DeprecatedString> render();
private:
static HashMap<DeprecatedString, DeprecatedString> parse_metadata(JsonObject const& metadata_object);

View File

@ -1,12 +1,12 @@
/*
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "PresenterWidget.h"
#include "Presentation.h"
#include <AK/Format.h>
#include <LibCore/MimeData.h>
#include <LibFileSystemAccessClient/Client.h>
#include <LibGUI/Action.h>
@ -15,13 +15,39 @@
#include <LibGUI/Menu.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Window.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Orientation.h>
PresenterWidget::PresenterWidget()
{
set_min_size(100, 100);
set_fill_with_background_color(true);
m_web_view = add<WebView::OutOfProcessWebView>();
m_web_view->set_frame_thickness(0);
m_web_view->set_scrollbars_enabled(false);
m_web_view->set_focus_policy(GUI::FocusPolicy::NoFocus);
m_web_view->set_content_scales_to_viewport(true);
}
void PresenterWidget::resize_event(GUI::ResizeEvent& event)
{
Widget::resize_event(event);
if (!m_current_presentation)
return;
auto normative_size = m_current_presentation->normative_size().to_type<float>();
float widget_ratio = static_cast<float>(event.size().width()) / static_cast<float>(event.size().height());
float wh_ratio = normative_size.width() / normative_size.height();
Gfx::IntRect rect;
if (widget_ratio >= wh_ratio) {
rect.set_width(static_cast<int>(ceilf(static_cast<float>(event.size().height()) * wh_ratio)));
rect.set_height(event.size().height());
} else {
float hw_ratio = normative_size.height() / normative_size.width();
rect.set_width(event.size().width());
rect.set_height(static_cast<int>(ceilf(static_cast<float>(event.size().width()) * hw_ratio)));
}
m_web_view->set_relative_rect(rect.centered_within(this->rect()));
}
ErrorOr<void> PresenterWidget::initialize_menubar()
@ -44,15 +70,13 @@ ErrorOr<void> PresenterWidget::initialize_menubar()
auto next_slide_action = GUI::Action::create("&Next", { KeyCode::Key_Right }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-forward.png"sv)), [this](auto&) {
if (m_current_presentation) {
m_current_presentation->next_frame();
outln("Switched forward to slide {} frame {}", m_current_presentation->current_slide_number(), m_current_presentation->current_frame_in_slide_number());
update();
update_web_view();
}
});
auto previous_slide_action = GUI::Action::create("&Previous", { KeyCode::Key_Left }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-back.png"sv)), [this](auto&) {
if (m_current_presentation) {
m_current_presentation->previous_frame();
outln("Switched backward to slide {} frame {}", m_current_presentation->current_slide_number(), m_current_presentation->current_frame_in_slide_number());
update();
update_web_view();
}
});
TRY(presentation_menu.try_add_action(next_slide_action));
@ -64,25 +88,31 @@ ErrorOr<void> PresenterWidget::initialize_menubar()
this->window()->set_fullscreen(true);
})));
TRY(presentation_menu.try_add_action(GUI::Action::create("Present From First &Slide", { KeyCode::Key_F5 }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/play.png"sv)), [this](auto&) {
if (m_current_presentation)
if (m_current_presentation) {
m_current_presentation->go_to_first_slide();
update_web_view();
}
this->window()->set_fullscreen(true);
})));
return {};
}
void PresenterWidget::update_web_view()
{
m_web_view->run_javascript(DeprecatedString::formatted("goto({}, {})", m_current_presentation->current_slide_number(), m_current_presentation->current_frame_in_slide_number()));
}
void PresenterWidget::set_file(StringView file_name)
{
auto presentation = Presentation::load_from_file(file_name, *window());
auto presentation = Presentation::load_from_file(file_name);
if (presentation.is_error()) {
GUI::MessageBox::show_error(window(), DeprecatedString::formatted("The presentation \"{}\" could not be loaded.\n{}", file_name, presentation.error()));
} else {
m_current_presentation = presentation.release_value();
window()->set_title(DeprecatedString::formatted(title_template, m_current_presentation->title(), m_current_presentation->author()));
set_min_size(m_current_presentation->normative_size());
// This will apply the new minimum size.
update();
m_web_view->load_html(MUST(m_current_presentation->render()), "presenter://slide.html"sv);
}
}
@ -114,24 +144,19 @@ void PresenterWidget::keydown_event(GUI::KeyEvent& event)
}
}
void PresenterWidget::paint_event([[maybe_unused]] GUI::PaintEvent& event)
void PresenterWidget::paint_event(GUI::PaintEvent& event)
{
GUI::Painter painter(*this);
painter.clear_rect(event.rect(), Gfx::Color::Black);
}
void PresenterWidget::second_paint_event(GUI::PaintEvent& event)
{
if (!m_current_presentation)
return;
auto normative_size = m_current_presentation->normative_size();
// Choose an aspect-correct size which doesn't exceed actual widget dimensions.
auto width_corresponding_to_height = height() * normative_size.aspect_ratio();
auto dimension_to_preserve = (width_corresponding_to_height > width()) ? Orientation::Horizontal : Orientation::Vertical;
auto display_size = size().match_aspect_ratio(normative_size.aspect_ratio(), dimension_to_preserve);
GUI::Painter painter { *this };
auto clip_rect = Gfx::IntRect::centered_at({ width() / 2, height() / 2 }, display_size);
painter.clear_clip_rect();
// FIXME: This currently leaves a black border when the window aspect ratio doesn't match.
// Figure out a way to apply the background color here as well.
painter.add_clip_rect(clip_rect);
m_current_presentation->paint(painter);
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.draw_text(m_web_view->relative_rect(), m_current_presentation->current_slide().title(), Gfx::TextAlignment::BottomCenter);
}
void PresenterWidget::drag_enter_event(GUI::DragEvent& event)

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -11,6 +12,7 @@
#include <LibGUI/Event.h>
#include <LibGUI/UIDimensions.h>
#include <LibGUI/Widget.h>
#include <LibWebView/OutOfProcessWebView.h>
// Title, Author
constexpr StringView const title_template = "{} ({}) — Presenter"sv;
@ -29,11 +31,17 @@ public:
protected:
virtual void paint_event(GUI::PaintEvent&) override;
virtual void second_paint_event(GUI::PaintEvent&) override;
virtual void keydown_event(GUI::KeyEvent&) override;
virtual void drag_enter_event(GUI::DragEvent&) override;
virtual void drop_event(GUI::DropEvent&) override;
virtual void resize_event(GUI::ResizeEvent&) override;
private:
void update_web_view();
RefPtr<WebView::OutOfProcessWebView> m_web_view;
OwnPtr<Presentation> m_current_presentation;
RefPtr<GUI::Action> m_next_slide_action;
RefPtr<GUI::Action> m_previous_slide_action;

View File

@ -1,16 +1,13 @@
/*
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Slide.h"
#include "Presentation.h"
#include <AK/JsonObject.h>
#include <AK/NonnullRefPtrVector.h>
#include <LibGUI/Window.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Size.h>
#include <LibGfx/TextAlignment.h>
Slide::Slide(NonnullRefPtrVector<SlideObject> slide_objects, DeprecatedString title)
: m_slide_objects(move(slide_objects))
@ -18,7 +15,7 @@ Slide::Slide(NonnullRefPtrVector<SlideObject> slide_objects, DeprecatedString ti
{
}
ErrorOr<Slide> Slide::parse_slide(JsonObject const& slide_json, NonnullRefPtr<GUI::Window> window)
ErrorOr<Slide> Slide::parse_slide(JsonObject const& slide_json)
{
// FIXME: Use the text with the "title" role for a title, if there is no title given.
auto title = slide_json.get("title"sv).as_string_or("Untitled slide");
@ -34,20 +31,18 @@ ErrorOr<Slide> Slide::parse_slide(JsonObject const& slide_json, NonnullRefPtr<GU
return Error::from_string_view("Slides must be objects"sv);
auto const& slide_object_json = maybe_slide_object_json.as_object();
auto slide_object = TRY(SlideObject::parse_slide_object(slide_object_json, window));
auto slide_object = TRY(SlideObject::parse_slide_object(slide_object_json));
slide_objects.append(move(slide_object));
}
return Slide { move(slide_objects), title };
}
void Slide::paint(Gfx::Painter& painter, unsigned int current_frame, Gfx::FloatSize display_scale) const
ErrorOr<HTMLElement> Slide::render(Presentation const& presentation) const
{
for (auto const& object : m_slide_objects) {
if (object.is_visible_during_frame(current_frame))
object.paint(painter, display_scale);
}
// FIXME: Move this to user settings.
painter.draw_text(painter.clip_rect(), title(), Gfx::TextAlignment::BottomCenter);
HTMLElement wrapper;
wrapper.tag_name = "div"sv;
for (auto const& object : m_slide_objects)
TRY(wrapper.children.try_append(TRY(object.render(presentation))));
return wrapper;
}

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -9,19 +10,18 @@
#include "SlideObject.h"
#include <AK/DeprecatedString.h>
#include <AK/Forward.h>
#include <AK/NonnullOwnPtrVector.h>
#include <LibGfx/Forward.h>
#include <AK/NonnullRefPtrVector.h>
// A single slide of a presentation.
class Slide final {
public:
static ErrorOr<Slide> parse_slide(JsonObject const& slide_json, NonnullRefPtr<GUI::Window> window);
static ErrorOr<Slide> parse_slide(JsonObject const& slide_json);
// FIXME: shouldn't be hard-coded to 1.
unsigned frame_count() const { return 1; }
StringView title() const { return m_title; }
void paint(Gfx::Painter&, unsigned current_frame, Gfx::FloatSize display_scale) const;
ErrorOr<HTMLElement> render(Presentation const&) const;
private:
Slide(NonnullRefPtrVector<SlideObject> slide_objects, DeprecatedString title);

View File

@ -5,23 +5,20 @@
*/
#include "SlideObject.h"
#include "Presentation.h"
#include <AK/JsonObject.h>
#include <AK/RefPtr.h>
#include <LibCore/Object.h>
#include <LibCore/Stream.h>
#include <LibGUI/Margins.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Orientation.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Size.h>
#include <LibGfx/TextWrapping.h>
#include <LibImageDecoderClient/Client.h>
#include <AK/URL.h>
#include <LibGfx/Font/FontStyleMapping.h>
#include <LibGfx/Rect.h>
ErrorOr<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject const& slide_object_json, NonnullRefPtr<GUI::Window> window)
static DeprecatedString to_css_length(float design_value, Presentation const& presentation)
{
auto image_decoder_client = TRY(ImageDecoderClient::Client::try_create());
float length_in_vw = design_value / static_cast<float>(presentation.normative_size().width()) * 100.0f;
return DeprecatedString::formatted("{}vw", length_in_vw);
}
ErrorOr<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject const& slide_object_json)
{
auto const& maybe_type = slide_object_json.get("type"sv);
if (!maybe_type.is_string())
return Error::from_string_view("Slide object must have a type"sv);
@ -31,127 +28,131 @@ ErrorOr<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject c
if (type == "text"sv)
object = TRY(try_make_ref_counted<Text>());
else if (type == "image"sv)
object = TRY(try_make_ref_counted<Image>(image_decoder_client, window));
object = TRY(try_make_ref_counted<Image>());
else
return Error::from_string_view("Unsupported slide object type"sv);
slide_object_json.for_each_member([&](auto const& key, auto const& value) {
if (key == "type"sv)
return;
auto successful = object->set_property(key, value);
if (!successful)
dbgln("Storing {:15} = {:20} on slide object type {:8} failed, ignoring.", key, value, type);
object->set_property(key, value);
});
return object.release_nonnull();
}
SlideObject::SlideObject()
void SlideObject::set_property(StringView name, JsonValue value)
{
REGISTER_RECT_PROPERTY("rect", rect, set_rect);
if (name == "rect"sv) {
if (value.is_array() && value.as_array().size() == 4) {
Gfx::IntRect rect;
rect.set_x(value.as_array()[0].to_i32());
rect.set_y(value.as_array()[1].to_i32());
rect.set_width(value.as_array()[2].to_i32());
rect.set_height(value.as_array()[3].to_i32());
m_rect = rect;
}
}
m_properties.set(name, move(value));
}
// FIXME: Consider drawing a placeholder box instead.
void SlideObject::paint(Gfx::Painter&, Gfx::FloatSize) const { }
Gfx::IntRect SlideObject::transformed_bounding_box(Gfx::IntRect clip_rect, Gfx::FloatSize display_scale) const
void GraphicsObject::set_property(StringView name, JsonValue value)
{
return m_rect.to_type<float>().scaled(display_scale.width(), display_scale.height()).to_rounded<int>().translated(clip_rect.top_left());
if (name == "color"sv) {
if (auto color = Gfx::Color::from_string(value.to_deprecated_string()); color.has_value()) {
m_color = color.release_value();
}
}
SlideObject::set_property(name, move(value));
}
GraphicsObject::GraphicsObject()
void Text::set_property(StringView name, JsonValue value)
{
register_property(
"color", [this]() { return this->color().to_deprecated_string(); },
[this](auto& value) {
auto color = Color::from_string(value.to_deprecated_string());
if (color.has_value()) {
this->set_color(color.value());
return true;
}
return false;
});
if (name == "text"sv) {
m_text = value.to_deprecated_string();
} else if (name == "font"sv) {
m_font_family = value.to_deprecated_string();
} else if (name == "font-weight"sv) {
m_font_weight = Gfx::name_to_weight(value.to_deprecated_string());
} else if (name == "font-size"sv) {
m_font_size_in_pt = value.to_float();
} else if (name == "text-alignment"sv) {
m_text_align = value.to_deprecated_string();
}
GraphicsObject::set_property(name, move(value));
}
Text::Text()
void Image::set_property(StringView name, JsonValue value)
{
REGISTER_STRING_PROPERTY("text", text, set_text);
REGISTER_FONT_WEIGHT_PROPERTY("font-weight", font_weight, set_font_weight);
REGISTER_TEXT_ALIGNMENT_PROPERTY("text-alignment", text_alignment, set_text_alignment);
REGISTER_INT_PROPERTY("font-size", font_size, set_font_size);
REGISTER_STRING_PROPERTY("font", font, set_font);
if (name == "path"sv) {
m_src = value.to_deprecated_string();
} else if (name == "scaling-mode"sv) {
if (value.to_deprecated_string() == "nearest-neighbor"sv)
m_image_rendering = "crisp-edges"sv;
else if (value.to_deprecated_string() == "smooth-pixels"sv)
m_image_rendering = "pixelated"sv;
}
SlideObject::set_property(name, move(value));
}
void Text::paint(Gfx::Painter& painter, Gfx::FloatSize display_scale) const
ErrorOr<HTMLElement> Text::render(Presentation const& presentation) const
{
auto scaled_bounding_box = this->transformed_bounding_box(painter.clip_rect(), display_scale);
auto scaled_font_size = display_scale.height() * static_cast<float>(m_font_size);
auto font = Gfx::FontDatabase::the().get(m_font, scaled_font_size, m_font_weight, 0, Gfx::Font::AllowInexactSizeMatch::Yes);
if (font.is_null())
font = Gfx::FontDatabase::default_font();
painter.draw_text(scaled_bounding_box, m_text.view(), *font, m_text_alignment, m_color, Gfx::TextElision::None, Gfx::TextWrapping::Wrap);
HTMLElement div;
div.tag_name = "div"sv;
div.style.set("color"sv, m_color.to_deprecated_string());
div.style.set("font-family"sv, DeprecatedString::formatted("'{}'", m_font_family));
div.style.set("font-size"sv, to_css_length(m_font_size_in_pt * 1.33333333f, presentation));
div.style.set("font-weight"sv, DeprecatedString::number(m_font_weight));
div.style.set("text-align"sv, m_text_align);
div.style.set("white-space"sv, "pre-wrap"sv);
div.style.set("width"sv, to_css_length(m_rect.width(), presentation));
div.style.set("height"sv, to_css_length(m_rect.height(), presentation));
div.style.set("position"sv, "absolute"sv);
div.style.set("left"sv, to_css_length(m_rect.left(), presentation));
div.style.set("top"sv, to_css_length(m_rect.top(), presentation));
div.inner_text = m_text;
return div;
}
Image::Image(NonnullRefPtr<ImageDecoderClient::Client> client, NonnullRefPtr<GUI::Window> window)
: m_client(move(client))
, m_window(move(window))
ErrorOr<HTMLElement> Image::render(Presentation const& presentation) const
{
REGISTER_STRING_PROPERTY("path", image_path, set_image_path);
REGISTER_ENUM_PROPERTY("scaling", scaling, set_scaling, ImageScaling,
{ ImageScaling::FitSmallest, "fit-smallest" },
{ ImageScaling::FitLargest, "fit-largest" },
{ ImageScaling::Stretch, "stretch" }, );
REGISTER_ENUM_PROPERTY("scaling-mode", scaling_mode, set_scaling_mode, Gfx::Painter::ScalingMode,
{ Gfx::Painter::ScalingMode::SmoothPixels, "smooth-pixels" },
{ Gfx::Painter::ScalingMode::NearestNeighbor, "nearest-neighbor" },
{ Gfx::Painter::ScalingMode::BilinearBlend, "bilinear-blend" }, );
HTMLElement img;
img.tag_name = "img"sv;
img.attributes.set("src"sv, URL::create_with_file_scheme(m_src).to_deprecated_string());
img.style.set("image-rendering"sv, m_image_rendering);
if (m_rect.width() > m_rect.height())
img.style.set("height"sv, "100%"sv);
else
img.style.set("width"sv, "100%"sv);
HTMLElement image_wrapper;
image_wrapper.tag_name = "div"sv;
image_wrapper.children.append(move(img));
image_wrapper.style.set("position"sv, "absolute"sv);
image_wrapper.style.set("left"sv, to_css_length(m_rect.left(), presentation));
image_wrapper.style.set("top"sv, to_css_length(m_rect.top(), presentation));
image_wrapper.style.set("width"sv, to_css_length(m_rect.width(), presentation));
image_wrapper.style.set("height"sv, to_css_length(m_rect.height(), presentation));
image_wrapper.style.set("text-align"sv, "center"sv);
return image_wrapper;
}
// FIXME: Should run on another thread and report errors.
ErrorOr<void> Image::reload_image()
ErrorOr<void> HTMLElement::serialize(StringBuilder& builder) const
{
auto file = TRY(Core::Stream::File::open(m_image_path, Core::Stream::OpenMode::Read));
auto data = TRY(file->read_until_eof());
auto maybe_decoded = m_client->decode_image(data);
if (!maybe_decoded.has_value() || maybe_decoded.value().frames.size() < 1)
return Error::from_string_view("Could not decode image"sv);
// FIXME: Handle multi-frame images.
m_currently_loaded_image = maybe_decoded.value().frames.first().bitmap;
TRY(builder.try_appendff("<{}", tag_name));
for (auto const& [key, value] : attributes) {
// FIXME: Escape the value string as necessary.
TRY(builder.try_appendff(" {}='{}'", key, value));
}
TRY(builder.try_append(" style=\""sv));
for (auto const& [key, value] : style) {
// FIXME: Escape the value string as necessary.
TRY(builder.try_appendff(" {}: {};", key, value));
}
TRY(builder.try_append("\">"sv));
if (!inner_text.is_empty())
TRY(builder.try_append(inner_text));
for (auto const& child : children) {
TRY(child.serialize(builder));
}
TRY(builder.try_appendff("</{}>", tag_name));
return {};
}
void Image::paint(Gfx::Painter& painter, Gfx::FloatSize display_scale) const
{
if (!m_currently_loaded_image)
return;
auto transformed_bounding_box = this->transformed_bounding_box(painter.clip_rect(), display_scale);
auto image_size = m_currently_loaded_image->size();
auto image_aspect_ratio = image_size.aspect_ratio();
auto image_box = transformed_bounding_box;
if (m_scaling != ImageScaling::Stretch) {
auto width_corresponding_to_height = image_box.height() * image_aspect_ratio;
auto direction_to_preserve_for_fit = width_corresponding_to_height > image_box.width() ? Orientation::Horizontal : Orientation::Vertical;
// Fit largest and fit smallest are the same, except with inverted preservation conditions.
if (m_scaling == ImageScaling::FitLargest)
direction_to_preserve_for_fit = direction_to_preserve_for_fit == Orientation::Vertical ? Orientation::Horizontal : Orientation::Vertical;
image_box.set_size(image_box.size().match_aspect_ratio(image_aspect_ratio, direction_to_preserve_for_fit));
}
image_box = image_box.centered_within(transformed_bounding_box);
auto original_clip_rect = painter.clip_rect();
painter.clear_clip_rect();
painter.add_clip_rect(image_box);
// FIXME: Allow to set the scaling mode.
painter.draw_scaled_bitmap(image_box, *m_currently_loaded_image, m_currently_loaded_image->rect(), 1.0f, m_scaling_mode);
painter.clear_clip_rect();
painter.add_clip_rect(original_clip_rect);
}

View File

@ -6,133 +6,76 @@
#pragma once
#include <AK/Forward.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <LibCore/Object.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Color.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Rect.h>
#include <LibGfx/TextAlignment.h>
#include <LibImageDecoderClient/Client.h>
class Presentation;
struct HTMLElement {
StringView tag_name;
HashMap<StringView, DeprecatedString> attributes;
HashMap<StringView, DeprecatedString> style;
DeprecatedString inner_text;
Vector<HTMLElement> children;
ErrorOr<void> serialize(StringBuilder&) const;
};
// Anything that can be on a slide.
// For properties set in the file, we re-use the Core::Object property facility.
class SlideObject : public Core::Object {
C_OBJECT_ABSTRACT(SlideObject);
class SlideObject : public RefCounted<SlideObject> {
public:
virtual ~SlideObject() = default;
static ErrorOr<NonnullRefPtr<SlideObject>> parse_slide_object(JsonObject const& slide_object_json, NonnullRefPtr<GUI::Window> window);
// FIXME: Actually determine this from the file data.
bool is_visible_during_frame([[maybe_unused]] unsigned frame_number) const { return true; }
virtual void paint(Gfx::Painter&, Gfx::FloatSize display_scale) const;
ALWAYS_INLINE Gfx::IntRect transformed_bounding_box(Gfx::IntRect clip_rect, Gfx::FloatSize display_scale) const;
void set_rect(Gfx::IntRect rect) { m_rect = rect; }
Gfx::IntRect rect() const { return m_rect; }
static ErrorOr<NonnullRefPtr<SlideObject>> parse_slide_object(JsonObject const& slide_object_json);
virtual ErrorOr<HTMLElement> render(Presentation const&) const = 0;
protected:
SlideObject();
SlideObject() = default;
virtual void set_property(StringView name, JsonValue);
HashMap<DeprecatedString, JsonValue> m_properties;
Gfx::IntRect m_rect;
};
// Objects with a foreground color.
class GraphicsObject : public SlideObject {
C_OBJECT_ABSTRACT(SlideObject);
public:
virtual ~GraphicsObject() = default;
void set_color(Gfx::Color color) { m_color = color; }
Gfx::Color color() const { return m_color; }
protected:
GraphicsObject();
GraphicsObject() = default;
virtual void set_property(StringView name, JsonValue) override;
// FIXME: Change the default color based on the color scheme
Gfx::Color m_color { Gfx::Color::Black };
};
class Text : public GraphicsObject {
C_OBJECT(SlideObject);
class Text final : public GraphicsObject {
public:
Text();
Text() = default;
virtual ~Text() = default;
virtual void paint(Gfx::Painter&, Gfx::FloatSize display_scale) const override;
private:
virtual ErrorOr<HTMLElement> render(Presentation const&) const override;
virtual void set_property(StringView name, JsonValue) override;
void set_font(DeprecatedString font) { m_font = move(font); }
StringView font() const { return m_font; }
void set_font_size(int font_size) { m_font_size = font_size; }
int font_size() const { return m_font_size; }
void set_font_weight(unsigned font_weight) { m_font_weight = font_weight; }
unsigned font_weight() const { return m_font_weight; }
void set_text_alignment(Gfx::TextAlignment text_alignment) { m_text_alignment = text_alignment; }
Gfx::TextAlignment text_alignment() const { return m_text_alignment; }
void set_text(DeprecatedString text) { m_text = move(text); }
StringView text() const { return m_text; }
protected:
DeprecatedString m_text;
// The font family, technically speaking.
DeprecatedString m_font;
int m_font_size { 18 };
DeprecatedString m_font_family;
DeprecatedString m_text_align;
float m_font_size_in_pt { 18 };
unsigned m_font_weight { Gfx::FontWeight::Regular };
Gfx::TextAlignment m_text_alignment { Gfx::TextAlignment::CenterLeft };
};
// How to scale an image object.
enum class ImageScaling {
// Fit the image into the bounding box, preserving its aspect ratio.
FitSmallest,
// Match the bounding box in width and height exactly; this will change the image's aspect ratio if the aspect ratio of the bounding box is not exactly the same.
Stretch,
// Make the image fill the bounding box, preserving its aspect ratio. This means that the image will be cut off on the top and bottom or left and right, depending on which dimension is "too large".
FitLargest,
};
class Image : public SlideObject {
C_OBJECT(Image);
class Image final : public SlideObject {
public:
Image(NonnullRefPtr<ImageDecoderClient::Client>, NonnullRefPtr<GUI::Window>);
Image() = default;
virtual ~Image() = default;
virtual void paint(Gfx::Painter&, Gfx::FloatSize display_scale) const override;
void set_image_path(DeprecatedString image_path)
{
m_image_path = move(image_path);
auto result = reload_image();
if (result.is_error())
GUI::MessageBox::show_error(m_window, DeprecatedString::formatted("Loading image {} failed: {}", m_image_path, result.error()));
}
StringView image_path() const { return m_image_path; }
void set_scaling(ImageScaling scaling) { m_scaling = scaling; }
ImageScaling scaling() const { return m_scaling; }
void set_scaling_mode(Gfx::Painter::ScalingMode scaling_mode) { m_scaling_mode = scaling_mode; }
Gfx::Painter::ScalingMode scaling_mode() const { return m_scaling_mode; }
protected:
DeprecatedString m_image_path;
ImageScaling m_scaling { ImageScaling::FitSmallest };
Gfx::Painter::ScalingMode m_scaling_mode { Gfx::Painter::ScalingMode::SmoothPixels };
private:
ErrorOr<void> reload_image();
DeprecatedString m_src;
StringView m_image_rendering;
RefPtr<Gfx::Bitmap> m_currently_loaded_image;
NonnullRefPtr<ImageDecoderClient::Client> m_client;
NonnullRefPtr<GUI::Window> m_window;
virtual ErrorOr<HTMLElement> render(Presentation const&) const override;
virtual void set_property(StringView name, JsonValue) override;
};

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -14,7 +15,7 @@
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
// rpath is required to load .presenter files, unix, sendfd and recvfd are required to talk to ImageDecoder and WindowServer.
// rpath is required to load .presenter files, unix, sendfd and recvfd are required to talk to WindowServer and WebContent.
TRY(Core::System::pledge("stdio rpath unix sendfd recvfd"));
DeprecatedString file_to_load;