From 02399d4775037d87231db05a887ecb093422e719 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Tue, 8 Mar 2022 06:00:04 -0600 Subject: [PATCH] PixelPaint: Add Image>Resize Image... dialog. (Front end) --- .../Applications/PixelPaint/CMakeLists.txt | 3 + Userland/Applications/PixelPaint/IconBag.cpp | 1 + Userland/Applications/PixelPaint/IconBag.h | 1 + Userland/Applications/PixelPaint/Image.cpp | 11 ++ Userland/Applications/PixelPaint/Image.h | 2 + Userland/Applications/PixelPaint/Layer.cpp | 25 ++++ Userland/Applications/PixelPaint/Layer.h | 2 + .../Applications/PixelPaint/MainWidget.cpp | 9 ++ .../PixelPaint/ResizeImageDialog.cpp | 111 ++++++++++++++++++ .../PixelPaint/ResizeImageDialog.gml | 108 +++++++++++++++++ .../PixelPaint/ResizeImageDialog.h | 29 +++++ 11 files changed, 302 insertions(+) create mode 100644 Userland/Applications/PixelPaint/ResizeImageDialog.cpp create mode 100644 Userland/Applications/PixelPaint/ResizeImageDialog.gml create mode 100644 Userland/Applications/PixelPaint/ResizeImageDialog.h diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index 62b181a825..f3df53ad9e 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -8,6 +8,7 @@ serenity_component( compile_gml(PixelPaintWindow.gml PixelPaintWindowGML.h pixel_paint_window_gml) compile_gml(EditGuideDialog.gml EditGuideDialogGML.h edit_guide_dialog_gml) compile_gml(FilterGallery.gml FilterGalleryGML.h filter_gallery_gml) +compile_gml(ResizeImageDialog.gml ResizeImageDialogGML.h resize_image_dialog_gml) set(SOURCES CreateNewImageDialog.cpp @@ -43,6 +44,8 @@ set(SOURCES PaletteWidget.cpp PixelPaintWindowGML.h ProjectLoader.cpp + ResizeImageDialog.cpp + ResizeImageDialogGML.h Selection.cpp ToolPropertiesWidget.cpp ToolboxWidget.cpp diff --git a/Userland/Applications/PixelPaint/IconBag.cpp b/Userland/Applications/PixelPaint/IconBag.cpp index 52ff362a6d..1312eeb189 100644 --- a/Userland/Applications/PixelPaint/IconBag.cpp +++ b/Userland/Applications/PixelPaint/IconBag.cpp @@ -26,6 +26,7 @@ ErrorOr IconBag::try_create() icon_bag.clear_guides = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/clear-guides.png")); icon_bag.edit_flip_vertical = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-flip-vertical.png")); icon_bag.edit_flip_horizontal = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-flip-horizontal.png")); + icon_bag.resize_image = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/selection-move.png")); icon_bag.crop = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/crop.png")); icon_bag.new_layer = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/new-layer.png")); icon_bag.previous_layer = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/previous-layer.png")); diff --git a/Userland/Applications/PixelPaint/IconBag.h b/Userland/Applications/PixelPaint/IconBag.h index 37327dff66..179d409a32 100644 --- a/Userland/Applications/PixelPaint/IconBag.h +++ b/Userland/Applications/PixelPaint/IconBag.h @@ -27,6 +27,7 @@ struct IconBag final { RefPtr clear_guides { nullptr }; RefPtr edit_flip_vertical { nullptr }; RefPtr edit_flip_horizontal { nullptr }; + RefPtr resize_image { nullptr }; RefPtr crop { nullptr }; RefPtr new_layer { nullptr }; RefPtr previous_layer { nullptr }; diff --git a/Userland/Applications/PixelPaint/Image.cpp b/Userland/Applications/PixelPaint/Image.cpp index f4832189e1..69d26e8637 100644 --- a/Userland/Applications/PixelPaint/Image.cpp +++ b/Userland/Applications/PixelPaint/Image.cpp @@ -527,6 +527,17 @@ void Image::crop(Gfx::IntRect const& cropped_rect) did_change_rect(cropped_rect); } +void Image::resize(Gfx::IntSize const& new_size, Gfx::Painter::ScalingMode scaling_mode) +{ + for (auto& layer : m_layers) { + layer.resize(new_size, scaling_mode); + } + + m_size = { new_size.width(), new_size.height() }; + did_change_rect(); + +} + Color Image::color_at(Gfx::IntPoint const& point) const { Color color; diff --git a/Userland/Applications/PixelPaint/Image.h b/Userland/Applications/PixelPaint/Image.h index 97e37eef5f..b6afe862da 100644 --- a/Userland/Applications/PixelPaint/Image.h +++ b/Userland/Applications/PixelPaint/Image.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -96,6 +97,7 @@ public: void flip(Gfx::Orientation orientation); void rotate(Gfx::RotationDirection direction); void crop(Gfx::IntRect const& rect); + void resize(Gfx::IntSize const& new_size, Gfx::Painter::ScalingMode scaling_mode); Color color_at(Gfx::IntPoint const& point) const; diff --git a/Userland/Applications/PixelPaint/Layer.cpp b/Userland/Applications/PixelPaint/Layer.cpp index 5298946c97..57b4099ba4 100644 --- a/Userland/Applications/PixelPaint/Layer.cpp +++ b/Userland/Applications/PixelPaint/Layer.cpp @@ -180,6 +180,31 @@ void Layer::crop(Gfx::IntRect const& rect) did_modify_bitmap(); } +void Layer::resize(Gfx::IntSize const& new_size, Gfx::Painter::ScalingMode scaling_mode) +{ + const Gfx::IntRect old_rect(Gfx::IntPoint(0, 0), size()); + const Gfx::IntRect new_rect(Gfx::IntPoint(0, 0), new_size); + + { + auto resized = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, new_size).release_value_but_fixme_should_propagate_errors(); + Gfx::Painter painter(resized); + + painter.draw_scaled_bitmap(new_rect, *m_content_bitmap, old_rect, 1.0f, scaling_mode); + + m_content_bitmap = move(resized); + } + + if (m_mask_bitmap) { + auto resized = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, new_size).release_value_but_fixme_should_propagate_errors(); + Gfx::Painter painter(resized); + + painter.draw_scaled_bitmap(new_rect, *m_mask_bitmap, old_rect, 1.0f, scaling_mode); + m_mask_bitmap = move(resized); + } + + did_modify_bitmap(); +} + void Layer::update_cached_bitmap() { if (!is_masked()) { diff --git a/Userland/Applications/PixelPaint/Layer.h b/Userland/Applications/PixelPaint/Layer.h index 55a92d1ae0..408fee937d 100644 --- a/Userland/Applications/PixelPaint/Layer.h +++ b/Userland/Applications/PixelPaint/Layer.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace PixelPaint { @@ -56,6 +57,7 @@ public: void flip(Gfx::Orientation orientation); void rotate(Gfx::RotationDirection direction); void crop(Gfx::IntRect const& rect); + void resize(Gfx::IntSize const& new_size, Gfx::Painter::ScalingMode scaling_mode); ErrorOr try_set_bitmaps(NonnullRefPtr content, RefPtr mask); diff --git a/Userland/Applications/PixelPaint/MainWidget.cpp b/Userland/Applications/PixelPaint/MainWidget.cpp index 18b2c5f504..6bc6c1ebe3 100644 --- a/Userland/Applications/PixelPaint/MainWidget.cpp +++ b/Userland/Applications/PixelPaint/MainWidget.cpp @@ -12,6 +12,7 @@ #include "EditGuideDialog.h" #include "FilterGallery.h" #include "FilterParams.h" +#include "ResizeImageDialog.h" #include #include #include @@ -499,6 +500,14 @@ void MainWidget::initialize_menubar(GUI::Window& window) editor->image().rotate(Gfx::RotationDirection::Clockwise); })); m_image_menu->add_separator(); + m_image_menu->add_action(GUI::Action::create( + "&Resize Image...", { Mod_Ctrl | Mod_Shift, Key_R }, g_icon_bag.resize_image, [&](auto&) { + auto* editor = current_image_editor(); + VERIFY(editor); + auto dialog = PixelPaint::ResizeImageDialog::construct(editor->image().size(), &window); + if (dialog->exec() == GUI::Dialog::ExecResult::OK) + editor->image().resize(dialog->desired_size(), dialog->scaling_mode()); + })); m_image_menu->add_action(GUI::Action::create( "&Crop To Selection", g_icon_bag.crop, [&](auto&) { auto* editor = current_image_editor(); diff --git a/Userland/Applications/PixelPaint/ResizeImageDialog.cpp b/Userland/Applications/PixelPaint/ResizeImageDialog.cpp new file mode 100644 index 0000000000..f255d3c038 --- /dev/null +++ b/Userland/Applications/PixelPaint/ResizeImageDialog.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022, Andrew Smith + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ResizeImageDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PixelPaint { + +ResizeImageDialog::ResizeImageDialog(Gfx::IntSize const& suggested_size, GUI::Window* parent_window) + : Dialog(parent_window) +{ + m_desired_size.set_width(max(1, suggested_size.width())); + m_desired_size.set_height(max(1, suggested_size.height())); + m_starting_aspect_ratio = m_desired_size.width() / static_cast(m_desired_size.height()); + + set_title("Resize Image"); + resize(260, 210); + set_icon(parent_window->icon()); + + auto& main_widget = set_main_widget(); + if (!main_widget.load_from_gml(resize_image_dialog_gml)) + VERIFY_NOT_REACHED(); + + auto width_spinbox = main_widget.find_descendant_of_type_named("width_spinbox"); + auto height_spinbox = main_widget.find_descendant_of_type_named("height_spinbox"); + auto keep_aspect_ratio_checkbox = main_widget.find_descendant_of_type_named("keep_aspect_ratio_checkbox"); + + VERIFY(width_spinbox); + VERIFY(height_spinbox); + VERIFY(keep_aspect_ratio_checkbox); + + width_spinbox->set_value(m_desired_size.width()); + width_spinbox->on_change = [this, height_spinbox, keep_aspect_ratio_checkbox](int value) { + if (keep_aspect_ratio_checkbox->is_checked()) { + int desired_height = static_cast(roundf(value / m_starting_aspect_ratio)); + height_spinbox->set_value(desired_height, GUI::AllowCallback::No); + m_desired_size.set_height(height_spinbox->value()); + } + m_desired_size.set_width(value); + }; + width_spinbox->on_return_pressed = [this]() { + done(ExecResult::OK); + }; + + height_spinbox->set_value(m_desired_size.height()); + height_spinbox->on_change = [this, width_spinbox, keep_aspect_ratio_checkbox](int value) { + if (keep_aspect_ratio_checkbox->is_checked()) { + int desired_width = static_cast(roundf(value * m_starting_aspect_ratio)); + width_spinbox->set_value(desired_width, GUI::AllowCallback::No); + m_desired_size.set_width(width_spinbox->value()); + } + m_desired_size.set_height(value); + }; + height_spinbox->on_return_pressed = [this]() { + done(ExecResult::OK); + }; + + keep_aspect_ratio_checkbox->on_checked = [this, height_spinbox](bool is_checked) { + if (is_checked) { + int desired_height = static_cast(roundf(m_desired_size.width() / m_starting_aspect_ratio)); + height_spinbox->set_value(desired_height, GUI::AllowCallback::No); + m_desired_size.set_height(height_spinbox->value()); + } + }; + + auto nearest_neighbor_radio = main_widget.find_descendant_of_type_named("nearest_neighbor_radio"); + auto bilinear_radio = main_widget.find_descendant_of_type_named("bilinear_radio"); + + VERIFY(nearest_neighbor_radio); + VERIFY(bilinear_radio); + + m_scaling_mode = Gfx::Painter::ScalingMode::NearestNeighbor; + if (bilinear_radio->is_checked()) { + m_scaling_mode = Gfx::Painter::ScalingMode::BilinearBlend; + } + + nearest_neighbor_radio->on_checked = [this](bool is_checked) { + if (is_checked) + m_scaling_mode = Gfx::Painter::ScalingMode::NearestNeighbor; + }; + bilinear_radio->on_checked = [this](bool is_checked) { + if (is_checked) + m_scaling_mode = Gfx::Painter::ScalingMode::BilinearBlend; + }; + + auto ok_button = main_widget.find_descendant_of_type_named("ok_button"); + auto cancel_button = main_widget.find_descendant_of_type_named("cancel_button"); + + VERIFY(ok_button); + VERIFY(cancel_button); + + ok_button->on_click = [this](auto) { + done(ExecResult::OK); + }; + + cancel_button->on_click = [this](auto) { + done(ExecResult::Cancel); + }; +} + +} diff --git a/Userland/Applications/PixelPaint/ResizeImageDialog.gml b/Userland/Applications/PixelPaint/ResizeImageDialog.gml new file mode 100644 index 0000000000..038e49212b --- /dev/null +++ b/Userland/Applications/PixelPaint/ResizeImageDialog.gml @@ -0,0 +1,108 @@ +@GUI::Widget { + fill_with_background_color: true + min_width: 260 + min_height: 210 + layout: @GUI::VerticalBoxLayout { + margins: [4] + } + + @GUI::GroupBox { + title: "Size (px)" + shrink_to_fit: true + layout: @GUI::VerticalBoxLayout { + margins: [4] + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 24 + + @GUI::Label { + text: "Width:" + fixed_width: 60 + text_alignment: "CenterRight" + } + + @GUI::SpinBox { + name: "width_spinbox" + min: 1 + max: 16384 + min_width: 140 + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 24 + + @GUI::Label { + text: "Height:" + fixed_width: 60 + text_alignment: "CenterRight" + } + + @GUI::SpinBox { + name: "height_spinbox" + min: 1 + max: 16384 + min_width: 140 + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 24 + min_width: 140 + + @GUI::Widget { + fixed_width: 60 + } + + @GUI::CheckBox { + name: "keep_aspect_ratio_checkbox" + text: "Keep aspect ratio" + checked: true + autosize: true + } + } + } + + @GUI::GroupBox { + title: "Scaling Mode" + shrink_to_fit: true + layout: @GUI::VerticalBoxLayout { + margins: [4] + } + + @GUI::RadioButton { + name: "nearest_neighbor_radio" + text: "Nearest neighbor" + checked: true + autosize: true + } + + @GUI::RadioButton { + name: "bilinear_radio" + text: "Bilinear" + autosize: true + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + + @GUI::Widget {} + + @GUI::Button { + name: "ok_button" + text: "OK" + max_width: 75 + } + + @GUI::Button { + name: "cancel_button" + text: "Cancel" + max_width: 75 + } + } +} diff --git a/Userland/Applications/PixelPaint/ResizeImageDialog.h b/Userland/Applications/PixelPaint/ResizeImageDialog.h new file mode 100644 index 0000000000..17887128e0 --- /dev/null +++ b/Userland/Applications/PixelPaint/ResizeImageDialog.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022, Andrew Smith + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace PixelPaint { + +class ResizeImageDialog final : public GUI::Dialog { + C_OBJECT(ResizeImageDialog); + +public: + Gfx::IntSize const& desired_size() const { return m_desired_size; } + Gfx::Painter::ScalingMode scaling_mode() const { return m_scaling_mode; } + +private: + ResizeImageDialog(Gfx::IntSize const& starting_size, GUI::Window* parent_window); + + Gfx::IntSize m_desired_size; + Gfx::Painter::ScalingMode m_scaling_mode; + float m_starting_aspect_ratio; +}; + +}