ImageViewer: Support displaying vector graphics (at arbitrary scales)

With this, you can scale, flip, and rotate vector graphics in the image
viewer like any other image, but with no pixelation :^)

With this change, vector graphics are decoded in-process (since there's
no standard way to encode them over IPC, a new encoding would be needed
for each format, which would be pretty much just be recreating that
format).

Raster images are still decoded out of process, so the surface area for
attack is still kept to a minimum.
This commit is contained in:
MacDue 2023-07-08 01:34:25 +01:00 committed by Andreas Kling
parent 57c81c1719
commit 4aa0ef9f98
3 changed files with 197 additions and 47 deletions

View file

@ -5,6 +5,7 @@
* Copyright (c) 2022, Mustafa Quraish <mustafa@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -20,12 +21,55 @@
#include <LibGUI/Application.h>
#include <LibGUI/MessageBox.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/ImageFormats/ImageDecoder.h>
#include <LibGfx/Orientation.h>
#include <LibGfx/Palette.h>
#include <LibImageDecoderClient/Client.h>
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<float> / 2));
else
apply_transform(Gfx::AffineTransform {}.rotate_radians(-AK::Pi<float> / 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<NonnullRefPtr<Gfx::Bitmap>> VectorImage::bitmap(Optional<Gfx::IntSize> 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<void> 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<Animation::Frame> 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<void> 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();
}
}

View file

@ -5,6 +5,7 @@
* Copyright (c) 2022, Mustafa Quraish <mustafa@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -14,10 +15,80 @@
#include <LibCore/Timer.h>
#include <LibGUI/AbstractZoomPanWidget.h>
#include <LibGUI/Painter.h>
#include <LibImageDecoderClient/Client.h>
#include <LibGfx/VectorGraphic.h>
namespace ImageViewer {
class Image : public RefCounted<Image> {
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<NonnullRefPtr<Gfx::Bitmap>> bitmap(Optional<Gfx::IntSize> ideal_size) const = 0;
virtual ~Image() = default;
};
class VectorImage final : public Image {
public:
static NonnullRefPtr<VectorImage> 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<NonnullRefPtr<Gfx::Bitmap>> bitmap(Optional<Gfx::IntSize> 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<Gfx::VectorGraphic> m_vector;
Gfx::IntSize m_size;
Gfx::AffineTransform m_transform;
};
class BitmapImage final : public Image {
public:
static NonnullRefPtr<BitmapImage> 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<NonnullRefPtr<Gfx::Bitmap>> bitmap(Optional<Gfx::IntSize>) const override
{
return m_bitmap;
}
private:
BitmapImage(Gfx::Bitmap& bitmap)
: m_bitmap(bitmap)
{
}
NonnullRefPtr<Gfx::Bitmap> 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<void()> on_doubleclick;
Function<void(const GUI::DropEvent&)> on_drop;
Function<void(Gfx::Bitmap const*)> on_image_change;
Function<void(Image*)> 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<DeprecatedString> load_files_from_directory(DeprecatedString const& path) const;
ErrorOr<void> try_open_file(String const&, Core::File&);
String m_path;
RefPtr<Gfx::Bitmap const> m_bitmap;
Optional<ImageDecoderClient::DecodedImage> m_decoded_image;
RefPtr<Image> m_image;
struct Animation {
struct Frame {
RefPtr<Image> image;
int duration { 0 };
};
size_t loop_count { 0 };
Vector<Frame> frames;
};
Optional<Animation> m_animation;
size_t m_current_frame_index { 0 };
size_t m_loops_completed { 0 };

View file

@ -77,12 +77,12 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
auto widget = TRY(root_widget->try_add<ViewWidget>());
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<int> 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<int> 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<int> 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);