1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 05:17:35 +00:00

PixelPaint: Add Image>Resize Image... dialog. (Front end)

This commit is contained in:
Andrew Smith 2022-03-08 06:00:04 -06:00 committed by Andreas Kling
parent abaecb878b
commit 02399d4775
11 changed files with 302 additions and 0 deletions

View file

@ -8,6 +8,7 @@ serenity_component(
compile_gml(PixelPaintWindow.gml PixelPaintWindowGML.h pixel_paint_window_gml) compile_gml(PixelPaintWindow.gml PixelPaintWindowGML.h pixel_paint_window_gml)
compile_gml(EditGuideDialog.gml EditGuideDialogGML.h edit_guide_dialog_gml) compile_gml(EditGuideDialog.gml EditGuideDialogGML.h edit_guide_dialog_gml)
compile_gml(FilterGallery.gml FilterGalleryGML.h filter_gallery_gml) compile_gml(FilterGallery.gml FilterGalleryGML.h filter_gallery_gml)
compile_gml(ResizeImageDialog.gml ResizeImageDialogGML.h resize_image_dialog_gml)
set(SOURCES set(SOURCES
CreateNewImageDialog.cpp CreateNewImageDialog.cpp
@ -43,6 +44,8 @@ set(SOURCES
PaletteWidget.cpp PaletteWidget.cpp
PixelPaintWindowGML.h PixelPaintWindowGML.h
ProjectLoader.cpp ProjectLoader.cpp
ResizeImageDialog.cpp
ResizeImageDialogGML.h
Selection.cpp Selection.cpp
ToolPropertiesWidget.cpp ToolPropertiesWidget.cpp
ToolboxWidget.cpp ToolboxWidget.cpp

View file

@ -26,6 +26,7 @@ ErrorOr<IconBag> IconBag::try_create()
icon_bag.clear_guides = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/clear-guides.png")); 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_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.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.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.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")); icon_bag.previous_layer = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/previous-layer.png"));

View file

@ -27,6 +27,7 @@ struct IconBag final {
RefPtr<Gfx::Bitmap> clear_guides { nullptr }; RefPtr<Gfx::Bitmap> clear_guides { nullptr };
RefPtr<Gfx::Bitmap> edit_flip_vertical { nullptr }; RefPtr<Gfx::Bitmap> edit_flip_vertical { nullptr };
RefPtr<Gfx::Bitmap> edit_flip_horizontal { nullptr }; RefPtr<Gfx::Bitmap> edit_flip_horizontal { nullptr };
RefPtr<Gfx::Bitmap> resize_image { nullptr };
RefPtr<Gfx::Bitmap> crop { nullptr }; RefPtr<Gfx::Bitmap> crop { nullptr };
RefPtr<Gfx::Bitmap> new_layer { nullptr }; RefPtr<Gfx::Bitmap> new_layer { nullptr };
RefPtr<Gfx::Bitmap> previous_layer { nullptr }; RefPtr<Gfx::Bitmap> previous_layer { nullptr };

View file

@ -527,6 +527,17 @@ void Image::crop(Gfx::IntRect const& cropped_rect)
did_change_rect(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 Image::color_at(Gfx::IntPoint const& point) const
{ {
Color color; Color color;

View file

@ -19,6 +19,7 @@
#include <LibGUI/Forward.h> #include <LibGUI/Forward.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
#include <LibGfx/Forward.h> #include <LibGfx/Forward.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Rect.h> #include <LibGfx/Rect.h>
#include <LibGfx/Size.h> #include <LibGfx/Size.h>
@ -96,6 +97,7 @@ public:
void flip(Gfx::Orientation orientation); void flip(Gfx::Orientation orientation);
void rotate(Gfx::RotationDirection direction); void rotate(Gfx::RotationDirection direction);
void crop(Gfx::IntRect const& rect); 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; Color color_at(Gfx::IntPoint const& point) const;

View file

@ -180,6 +180,31 @@ void Layer::crop(Gfx::IntRect const& rect)
did_modify_bitmap(); 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() void Layer::update_cached_bitmap()
{ {
if (!is_masked()) { if (!is_masked()) {

View file

@ -13,6 +13,7 @@
#include <AK/String.h> #include <AK/String.h>
#include <AK/Weakable.h> #include <AK/Weakable.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
#include <LibGfx/Painter.h>
namespace PixelPaint { namespace PixelPaint {
@ -56,6 +57,7 @@ public:
void flip(Gfx::Orientation orientation); void flip(Gfx::Orientation orientation);
void rotate(Gfx::RotationDirection direction); void rotate(Gfx::RotationDirection direction);
void crop(Gfx::IntRect const& rect); void crop(Gfx::IntRect const& rect);
void resize(Gfx::IntSize const& new_size, Gfx::Painter::ScalingMode scaling_mode);
ErrorOr<void> try_set_bitmaps(NonnullRefPtr<Gfx::Bitmap> content, RefPtr<Gfx::Bitmap> mask); ErrorOr<void> try_set_bitmaps(NonnullRefPtr<Gfx::Bitmap> content, RefPtr<Gfx::Bitmap> mask);

View file

@ -12,6 +12,7 @@
#include "EditGuideDialog.h" #include "EditGuideDialog.h"
#include "FilterGallery.h" #include "FilterGallery.h"
#include "FilterParams.h" #include "FilterParams.h"
#include "ResizeImageDialog.h"
#include <Applications/PixelPaint/PixelPaintWindowGML.h> #include <Applications/PixelPaint/PixelPaintWindowGML.h>
#include <LibConfig/Client.h> #include <LibConfig/Client.h>
#include <LibCore/File.h> #include <LibCore/File.h>
@ -499,6 +500,14 @@ void MainWidget::initialize_menubar(GUI::Window& window)
editor->image().rotate(Gfx::RotationDirection::Clockwise); editor->image().rotate(Gfx::RotationDirection::Clockwise);
})); }));
m_image_menu->add_separator(); 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( m_image_menu->add_action(GUI::Action::create(
"&Crop To Selection", g_icon_bag.crop, [&](auto&) { "&Crop To Selection", g_icon_bag.crop, [&](auto&) {
auto* editor = current_image_editor(); auto* editor = current_image_editor();

View file

@ -0,0 +1,111 @@
/*
* Copyright (c) 2022, Andrew Smith <andrew@alsmith.net>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "ResizeImageDialog.h"
#include <Applications/PixelPaint/ResizeImageDialogGML.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/RadioButton.h>
#include <LibGUI/SpinBox.h>
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<float>(m_desired_size.height());
set_title("Resize Image");
resize(260, 210);
set_icon(parent_window->icon());
auto& main_widget = set_main_widget<GUI::Widget>();
if (!main_widget.load_from_gml(resize_image_dialog_gml))
VERIFY_NOT_REACHED();
auto width_spinbox = main_widget.find_descendant_of_type_named<GUI::SpinBox>("width_spinbox");
auto height_spinbox = main_widget.find_descendant_of_type_named<GUI::SpinBox>("height_spinbox");
auto keep_aspect_ratio_checkbox = main_widget.find_descendant_of_type_named<GUI::CheckBox>("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<int>(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<int>(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<int>(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<GUI::RadioButton>("nearest_neighbor_radio");
auto bilinear_radio = main_widget.find_descendant_of_type_named<GUI::RadioButton>("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<GUI::Button>("ok_button");
auto cancel_button = main_widget.find_descendant_of_type_named<GUI::Button>("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);
};
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2022, Andrew Smith <andrew@alsmith.net>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGUI/Dialog.h>
#include <LibGfx/Painter.h>
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;
};
}