diff --git a/Userland/Applications/ImageViewer/ViewWidget.cpp b/Userland/Applications/ImageViewer/ViewWidget.cpp index c48b1dc66c..05e18ba7ba 100644 --- a/Userland/Applications/ImageViewer/ViewWidget.cpp +++ b/Userland/Applications/ImageViewer/ViewWidget.cpp @@ -5,6 +5,7 @@ * Copyright (c) 2022, Mustafa Quraish * Copyright (c) 2022, the SerenityOS developers. * Copyright (c) 2023, Caoimhe Byrne + * Copyright (c) 2023, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ @@ -20,12 +21,55 @@ #include #include #include +#include #include #include #include namespace ImageViewer { +void VectorImage::flip(Gfx::Orientation orientation) +{ + if (orientation == Gfx::Orientation::Horizontal) + apply_transform(Gfx::AffineTransform {}.scale(-1, 1)); + else + apply_transform(Gfx::AffineTransform {}.scale(1, -1)); +} + +void VectorImage::rotate(Gfx::RotationDirection rotation_direction) +{ + if (rotation_direction == Gfx::RotationDirection::Clockwise) + apply_transform(Gfx::AffineTransform {}.rotate_radians(AK::Pi / 2)); + else + apply_transform(Gfx::AffineTransform {}.rotate_radians(-AK::Pi / 2)); + m_size = { m_size.height(), m_size.width() }; +} + +void VectorImage::draw_into(Gfx::Painter& painter, Gfx::IntRect const& dest, Gfx::Painter::ScalingMode) const +{ + m_vector->draw_into(painter, dest, m_transform); +} + +ErrorOr> VectorImage::bitmap(Optional ideal_size) const +{ + return m_vector->bitmap(ideal_size.value_or(size()), m_transform); +} + +void BitmapImage::flip(Gfx::Orientation orientation) +{ + m_bitmap = m_bitmap->flipped(orientation).release_value_but_fixme_should_propagate_errors(); +} + +void BitmapImage::rotate(Gfx::RotationDirection rotation) +{ + m_bitmap = m_bitmap->rotated(rotation).release_value_but_fixme_should_propagate_errors(); +} + +void BitmapImage::draw_into(Gfx::Painter& painter, Gfx::IntRect const& dest, Gfx::Painter::ScalingMode scaling_mode) const +{ + painter.draw_scaled_bitmap(dest, *m_bitmap, m_bitmap->rect(), 1.0f, scaling_mode); +} + ViewWidget::ViewWidget() : m_timer(Core::Timer::try_create().release_value_but_fixme_should_propagate_errors()) { @@ -35,10 +79,10 @@ ViewWidget::ViewWidget() void ViewWidget::clear() { m_timer->stop(); - m_decoded_image.clear(); - m_bitmap = nullptr; + m_animation.clear(); + m_image = nullptr; if (on_image_change) - on_image_change(m_bitmap); + on_image_change(m_image); set_original_rect({}); m_path = {}; @@ -48,13 +92,13 @@ void ViewWidget::clear() void ViewWidget::flip(Gfx::Orientation orientation) { - m_bitmap = m_bitmap->flipped(orientation).release_value_but_fixme_should_propagate_errors(); + m_image->flip(orientation); scale_image_for_window(); } void ViewWidget::rotate(Gfx::RotationDirection rotation_direction) { - m_bitmap = m_bitmap->rotated(rotation_direction).release_value_but_fixme_should_propagate_errors(); + m_image->rotate(rotation_direction); scale_image_for_window(); } @@ -143,8 +187,8 @@ void ViewWidget::paint_event(GUI::PaintEvent& event) Gfx::StylePainter::paint_transparency_grid(painter, frame_inner_rect(), palette()); - if (!m_bitmap.is_null()) - painter.draw_scaled_bitmap(content_rect(), *m_bitmap, m_bitmap->rect(), 1.0f, m_scaling_mode); + if (m_image) + return m_image->draw_into(painter, content_rect(), m_scaling_mode); } void ViewWidget::mousedown_event(GUI::MouseEvent& event) @@ -174,24 +218,47 @@ void ViewWidget::open_file(String const& path, Core::File& file) ErrorOr ViewWidget::try_open_file(String const& path, Core::File& file) { - // Spawn a new ImageDecoder service process and connect to it. - auto client = TRY(ImageDecoderClient::Client::try_create()); - auto mime_type = Core::guess_mime_type_based_on_filename(path); - auto decoded_image_or_none = client->decode_image(TRY(file.read_until_eof()), mime_type); - if (!decoded_image_or_none.has_value()) { - return Error::from_string_literal("Failed to decode image"); + auto file_data = TRY(file.read_until_eof()); + bool is_animated = false; + size_t loop_count = 0; + Vector frames; + // Note: Doing this check only requires reading the header of images + // (so if the image is not vector graphics it can be still be decoded OOP). + if (auto decoder = Gfx::ImageDecoder::try_create_for_raw_bytes(file_data); decoder && decoder->is_vector()) { + // Use in-process decoding for vector graphics. + is_animated = decoder->is_animated(); + loop_count = decoder->loop_count(); + frames.ensure_capacity(decoder->frame_count()); + for (u32 i = 0; i < decoder->frame_count(); i++) { + auto frame_data = TRY(decoder->vector_frame(i)); + frames.unchecked_append({ VectorImage::create(*frame_data.image), frame_data.duration }); + } + } else { + // Use out-of-process decoding for raster formats. + auto client = TRY(ImageDecoderClient::Client::try_create()); + auto mime_type = Core::guess_mime_type_based_on_filename(path); + auto decoded_image = client->decode_image(file_data, mime_type); + if (!decoded_image.has_value()) { + return Error::from_string_literal("Failed to decode image"); + } + is_animated = decoded_image->is_animated; + loop_count = decoded_image->loop_count; + frames.ensure_capacity(decoded_image->frames.size()); + for (u32 i = 0; i < decoded_image->frames.size(); i++) { + auto& frame_data = decoded_image->frames[i]; + frames.unchecked_append({ BitmapImage::create(*frame_data.bitmap), int(frame_data.duration) }); + } } - m_decoded_image = decoded_image_or_none.release_value(); - m_bitmap = m_decoded_image->frames[0].bitmap; - if (m_bitmap.is_null()) { - return Error::from_string_literal("Image didn't contain a bitmap"); + m_image = frames[0].image; + if (is_animated && frames.size() > 1) { + m_animation = Animation { loop_count, move(frames) }; } - set_original_rect(m_bitmap->rect()); + set_original_rect(m_image->rect()); - if (m_decoded_image->is_animated && m_decoded_image->frames.size() > 1) { - auto const& first_frame = m_decoded_image->frames[0]; + if (m_animation.has_value()) { + auto const& first_frame = m_animation->frames[0]; m_timer->set_interval(first_frame.duration); m_timer->on_timeout = [this] { animate(); }; m_timer->start(); @@ -203,7 +270,7 @@ ErrorOr ViewWidget::try_open_file(String const& path, Core::File& file) GUI::Application::the()->set_most_recently_open_file(path); if (on_image_change) - on_image_change(m_bitmap); + on_image_change(m_image); if (scaled_for_first_image()) scale_image_for_window(); @@ -235,10 +302,10 @@ void ViewWidget::resize_event(GUI::ResizeEvent& event) void ViewWidget::scale_image_for_window() { - if (!m_bitmap) + if (!m_image) return; - set_original_rect(m_bitmap->rect()); + set_original_rect(m_image->rect()); fit_content_to_view(GUI::AbstractZoomPanWidget::FitType::Both); } @@ -250,7 +317,7 @@ void ViewWidget::resize_window() auto absolute_bitmap_rect = content_rect(); absolute_bitmap_rect.translate_by(window()->rect().top_left()); - if (!m_bitmap) + if (!m_image) return; auto new_size = content_rect().size(); @@ -270,33 +337,33 @@ void ViewWidget::resize_window() scale_image_for_window(); } -void ViewWidget::set_bitmap(Gfx::Bitmap const* bitmap) +void ViewWidget::set_image(Image const* image) { - if (m_bitmap == bitmap) + if (m_image == image) return; - m_bitmap = bitmap; - set_original_rect(m_bitmap->rect()); + m_image = image; + set_original_rect(m_image->rect()); update(); } // Same as ImageWidget::animate(), you probably want to keep any changes in sync void ViewWidget::animate() { - if (!m_decoded_image.has_value()) + if (!m_animation.has_value()) return; - m_current_frame_index = (m_current_frame_index + 1) % m_decoded_image->frames.size(); + m_current_frame_index = (m_current_frame_index + 1) % m_animation->frames.size(); - auto const& current_frame = m_decoded_image->frames[m_current_frame_index]; - set_bitmap(current_frame.bitmap); + auto const& current_frame = m_animation->frames[m_current_frame_index]; + set_image(current_frame.image); if ((int)current_frame.duration != m_timer->interval()) { m_timer->restart(current_frame.duration); } - if (m_current_frame_index == m_decoded_image->frames.size() - 1) { + if (m_current_frame_index == m_animation->frames.size() - 1) { ++m_loops_completed; - if (m_loops_completed > 0 && m_loops_completed == m_decoded_image->loop_count) { + if (m_loops_completed > 0 && m_loops_completed == m_animation->loop_count) { m_timer->stop(); } } diff --git a/Userland/Applications/ImageViewer/ViewWidget.h b/Userland/Applications/ImageViewer/ViewWidget.h index 96ff096c17..44cff4853a 100644 --- a/Userland/Applications/ImageViewer/ViewWidget.h +++ b/Userland/Applications/ImageViewer/ViewWidget.h @@ -5,6 +5,7 @@ * Copyright (c) 2022, Mustafa Quraish * Copyright (c) 2022, the SerenityOS developers. * Copyright (c) 2023, Caoimhe Byrne + * Copyright (c) 2023, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ @@ -14,10 +15,80 @@ #include #include #include -#include +#include namespace ImageViewer { +class Image : public RefCounted { +public: + virtual Gfx::IntSize size() const = 0; + virtual Gfx::IntRect rect() const { return { {}, size() }; } + + virtual void flip(Gfx::Orientation) = 0; + virtual void rotate(Gfx::RotationDirection) = 0; + + virtual void draw_into(Gfx::Painter&, Gfx::IntRect const& dest, Gfx::Painter::ScalingMode) const = 0; + + virtual ErrorOr> bitmap(Optional ideal_size) const = 0; + + virtual ~Image() = default; +}; + +class VectorImage final : public Image { +public: + static NonnullRefPtr create(Gfx::VectorGraphic& vector) { return adopt_ref(*new VectorImage(vector)); } + + virtual Gfx::IntSize size() const override { return m_size; } + + virtual void flip(Gfx::Orientation) override; + virtual void rotate(Gfx::RotationDirection) override; + + virtual void draw_into(Gfx::Painter&, Gfx::IntRect const& dest, Gfx::Painter::ScalingMode) const override; + + virtual ErrorOr> bitmap(Optional ideal_size) const override; + +private: + VectorImage(Gfx::VectorGraphic& vector) + : m_vector(vector) + , m_size(vector.size()) + { + } + + void apply_transform(Gfx::AffineTransform transform) + { + m_transform = transform.multiply(m_transform); + } + + NonnullRefPtr m_vector; + Gfx::IntSize m_size; + Gfx::AffineTransform m_transform; +}; + +class BitmapImage final : public Image { +public: + static NonnullRefPtr create(Gfx::Bitmap& bitmap) { return adopt_ref(*new BitmapImage(bitmap)); } + + virtual Gfx::IntSize size() const override { return m_bitmap->size(); } + + virtual void flip(Gfx::Orientation) override; + virtual void rotate(Gfx::RotationDirection) override; + + virtual void draw_into(Gfx::Painter&, Gfx::IntRect const& dest, Gfx::Painter::ScalingMode) const override; + + virtual ErrorOr> bitmap(Optional) const override + { + return m_bitmap; + } + +private: + BitmapImage(Gfx::Bitmap& bitmap) + : m_bitmap(bitmap) + { + } + + NonnullRefPtr m_bitmap; +}; + class ViewWidget final : public GUI::AbstractZoomPanWidget { C_OBJECT(ViewWidget) public: @@ -30,7 +101,7 @@ public: virtual ~ViewWidget() override = default; - Gfx::Bitmap const* bitmap() const { return m_bitmap.ptr(); } + Image const* image() const { return m_image.ptr(); } String const& path() const { return m_path; } void set_toolbar_height(int height) { m_toolbar_height = height; } int toolbar_height() { return m_toolbar_height; } @@ -52,7 +123,8 @@ public: Function on_doubleclick; Function on_drop; - Function on_image_change; + + Function on_image_change; private: ViewWidget(); @@ -64,14 +136,25 @@ private: virtual void drop_event(GUI::DropEvent&) override; virtual void resize_event(GUI::ResizeEvent&) override; - void set_bitmap(Gfx::Bitmap const* bitmap); + void set_image(Image const* image); void animate(); Vector load_files_from_directory(DeprecatedString const& path) const; ErrorOr try_open_file(String const&, Core::File&); String m_path; - RefPtr m_bitmap; - Optional m_decoded_image; + RefPtr m_image; + + struct Animation { + struct Frame { + RefPtr image; + int duration { 0 }; + }; + + size_t loop_count { 0 }; + Vector frames; + }; + + Optional m_animation; size_t m_current_frame_index { 0 }; size_t m_loops_completed { 0 }; diff --git a/Userland/Applications/ImageViewer/main.cpp b/Userland/Applications/ImageViewer/main.cpp index cad31b99bd..4f8d501e9f 100644 --- a/Userland/Applications/ImageViewer/main.cpp +++ b/Userland/Applications/ImageViewer/main.cpp @@ -77,12 +77,12 @@ ErrorOr serenity_main(Main::Arguments arguments) auto widget = TRY(root_widget->try_add()); widget->on_scale_change = [&](float scale) { - if (!widget->bitmap()) { + if (!widget->image()) { window->set_title("Image Viewer"); return; } - window->set_title(DeprecatedString::formatted("{} {} {}% - Image Viewer", widget->path(), widget->bitmap()->size().to_deprecated_string(), (int)(scale * 100))); + window->set_title(DeprecatedString::formatted("{} {} {}% - Image Viewer", widget->path(), widget->image()->size().to_deprecated_string(), (int)(scale * 100))); if (!widget->scaled_for_first_image()) { widget->set_scaled_for_first_image(true); @@ -186,7 +186,7 @@ ErrorOr serenity_main(Main::Arguments arguments) auto desktop_wallpaper_action = GUI::Action::create("Set as Desktop &Wallpaper", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-display-settings.png"sv)), [&](auto&) { - if (!GUI::Desktop::the().set_wallpaper(widget->bitmap(), widget->path())) { + if (!GUI::Desktop::the().set_wallpaper(widget->image()->bitmap(GUI::Desktop::the().rect().size()).release_value_but_fixme_should_propagate_errors(), widget->path())) { GUI::MessageBox::show(window, DeprecatedString::formatted("set_wallpaper({}) failed", widget->path()), "Could not set wallpaper"sv, @@ -249,8 +249,8 @@ ErrorOr serenity_main(Main::Arguments arguments) hide_show_toolbar_action->set_checked(true); auto copy_action = GUI::CommonActions::make_copy_action([&](auto&) { - if (widget->bitmap()) - GUI::Clipboard::the().set_bitmap(*widget->bitmap()); + if (widget->image()) + GUI::Clipboard::the().set_bitmap(*widget->image()->bitmap({}).release_value_but_fixme_should_propagate_errors()); }); auto nearest_neighbor_action = GUI::Action::create_checkable("&Nearest Neighbor", [&](auto&) { @@ -270,8 +270,8 @@ ErrorOr serenity_main(Main::Arguments arguments) widget->set_scaling_mode(Gfx::Painter::ScalingMode::BoxSampling); }); - widget->on_image_change = [&](Gfx::Bitmap const* bitmap) { - bool should_enable_image_actions = (bitmap != nullptr); + widget->on_image_change = [&](Image const* image) { + bool should_enable_image_actions = (image != nullptr); bool should_enable_forward_actions = (widget->is_next_available() && should_enable_image_actions); bool should_enable_backward_actions = (widget->is_previous_available() && should_enable_image_actions); delete_action->set_enabled(should_enable_image_actions);