From 5aeb6552f08a8183139d9c0104e5b5d9ad3c986b Mon Sep 17 00:00:00 2001 From: Torstennator Date: Sun, 24 Apr 2022 10:46:59 +0200 Subject: [PATCH] PixelPaint: Add level sliders for brightness, contrast and gamma This patch adds a basic dialog to change brightness, contrast and gamma correction for the selected layer. --- Base/res/icons/pixelpaint/levels.png | Bin 0 -> 189 bytes .../Applications/PixelPaint/CMakeLists.txt | 3 + Userland/Applications/PixelPaint/IconBag.cpp | 1 + Userland/Applications/PixelPaint/IconBag.h | 1 + .../Applications/PixelPaint/LevelsDialog.cpp | 158 ++++++++++++++++++ .../Applications/PixelPaint/LevelsDialog.gml | 87 ++++++++++ .../Applications/PixelPaint/LevelsDialog.h | 38 +++++ .../Applications/PixelPaint/MainWidget.cpp | 14 ++ Userland/Applications/PixelPaint/MainWidget.h | 1 + 9 files changed, 303 insertions(+) create mode 100644 Base/res/icons/pixelpaint/levels.png create mode 100644 Userland/Applications/PixelPaint/LevelsDialog.cpp create mode 100644 Userland/Applications/PixelPaint/LevelsDialog.gml create mode 100644 Userland/Applications/PixelPaint/LevelsDialog.h diff --git a/Base/res/icons/pixelpaint/levels.png b/Base/res/icons/pixelpaint/levels.png new file mode 100644 index 0000000000000000000000000000000000000000..698c5167f36cbb57ba95d5426926db07ac581d25 GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0y~yV2}b~7G?$phUk>QzYGivLIFM@t_%ze4h{|t4Gjkl z9Jqh~{{R2~_y2cYz`(%3SQ6wH%;50sMj8VHL!_sRV~E7%bP0l+XkKn(IED literal 0 HcmV?d00001 diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index f3df53ad9e..76b224bbf3 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -9,6 +9,7 @@ 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) +compile_gml(LevelsDialog.gml LevelsDialogGML.h levels_dialog_gml) set(SOURCES CreateNewImageDialog.cpp @@ -39,6 +40,8 @@ set(SOURCES Layer.cpp LayerListWidget.cpp LayerPropertiesWidget.cpp + LevelsDialogGML.h + LevelsDialog.cpp MainWidget.cpp Mask.cpp PaletteWidget.cpp diff --git a/Userland/Applications/PixelPaint/IconBag.cpp b/Userland/Applications/PixelPaint/IconBag.cpp index 1312eeb189..ec0d45f1cd 100644 --- a/Userland/Applications/PixelPaint/IconBag.cpp +++ b/Userland/Applications/PixelPaint/IconBag.cpp @@ -40,6 +40,7 @@ ErrorOr IconBag::try_create() icon_bag.merge_active_layer_up = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/merge-active-layer-up.png")); icon_bag.merge_active_layer_down = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/merge-active-layer-down.png")); icon_bag.filter = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/filter.png")); + icon_bag.levels = TRY(Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/levels.png")); return icon_bag; } diff --git a/Userland/Applications/PixelPaint/IconBag.h b/Userland/Applications/PixelPaint/IconBag.h index 179d409a32..97e5615464 100644 --- a/Userland/Applications/PixelPaint/IconBag.h +++ b/Userland/Applications/PixelPaint/IconBag.h @@ -41,5 +41,6 @@ struct IconBag final { RefPtr merge_active_layer_up { nullptr }; RefPtr merge_active_layer_down { nullptr }; RefPtr filter { nullptr }; + RefPtr levels { nullptr }; }; } diff --git a/Userland/Applications/PixelPaint/LevelsDialog.cpp b/Userland/Applications/PixelPaint/LevelsDialog.cpp new file mode 100644 index 0000000000..ec20a8e191 --- /dev/null +++ b/Userland/Applications/PixelPaint/LevelsDialog.cpp @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2022, Torsten Engelmann + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "LevelsDialog.h" +#include +#include +#include +#include + +namespace PixelPaint { + +LevelsDialog::LevelsDialog(GUI::Window* parent_window, ImageEditor* editor) + : GUI::Dialog(parent_window) +{ + set_title("Levels"); + set_icon(parent_window->icon()); + + auto& main_widget = set_main_widget(); + if (!main_widget.load_from_gml(levels_dialog_gml)) + VERIFY_NOT_REACHED(); + + resize(305, 202); + set_resizable(false); + + m_editor = editor; + + m_brightness_slider = main_widget.find_descendant_of_type_named("brightness_slider"); + m_contrast_slider = main_widget.find_descendant_of_type_named("contrast_slider"); + m_gamma_slider = main_widget.find_descendant_of_type_named("gamma_slider"); + auto context_label = main_widget.find_descendant_of_type_named("context_label"); + auto apply_button = main_widget.find_descendant_of_type_named("apply_button"); + auto cancel_button = main_widget.find_descendant_of_type_named("cancel_button"); + + VERIFY(m_brightness_slider); + VERIFY(m_contrast_slider); + VERIFY(m_gamma_slider); + VERIFY(context_label); + VERIFY(apply_button); + VERIFY(cancel_button); + VERIFY(m_editor->active_layer()); + + context_label->set_text(String::formatted("Working on layer: {}", m_editor->active_layer()->name())); + m_gamma_slider->set_value(100); + + m_brightness_slider->on_change = [this](auto) { + generate_new_image(); + }; + + m_contrast_slider->on_change = [this](auto) { + generate_new_image(); + }; + + m_gamma_slider->on_change = [this](auto) { + generate_new_image(); + }; + + apply_button->on_click = [this](auto) { + if (m_did_change) { + m_editor->on_modified_change(true); + m_editor->did_complete_action(); + } + + cleanup_resources(); + done(ExecResult::OK); + }; + + cancel_button->on_click = [this](auto) { + done(ExecResult::Cancel); + }; +} + +void LevelsDialog::revert_possible_changes() +{ + // FIXME: Find a faster way to revert all the changes that we have done. + if (m_did_change && m_reference_bitmap) { + for (int x = 0; x < m_reference_bitmap->width(); x++) { + for (int y = 0; y < m_reference_bitmap->height(); y++) { + m_editor->active_layer()->content_bitmap().set_pixel(x, y, m_reference_bitmap->get_pixel(x, y)); + } + } + m_editor->layers_did_change(); + } + + cleanup_resources(); +} + +void LevelsDialog::generate_new_image() +{ + (void)ensure_reference_bitmap(); + if (m_reference_bitmap.is_null()) + return; + + generate_precomputed_color_correction(); + Color current_pixel_color; + Color new_pixel_color; + Gfx::StorageFormat storage_format = Gfx::determine_storage_format(m_editor->active_layer()->content_bitmap().format()); + + for (int x = 0; x < m_reference_bitmap->width(); x++) { + for (int y = 0; y < m_reference_bitmap->height(); y++) { + current_pixel_color = m_reference_bitmap->get_pixel(x, y); + + new_pixel_color.set_alpha(current_pixel_color.alpha()); + new_pixel_color.set_red(m_precomputed_color_correction[current_pixel_color.red()]); + new_pixel_color.set_green(m_precomputed_color_correction[current_pixel_color.green()]); + new_pixel_color.set_blue(m_precomputed_color_correction[current_pixel_color.blue()]); + + switch (storage_format) { + case Gfx::StorageFormat::BGRx8888: + case Gfx::StorageFormat::BGRA8888: + m_editor->active_layer()->content_bitmap().scanline(y)[x] = new_pixel_color.value(); + break; + default: + m_editor->active_layer()->content_bitmap().set_pixel(x, y, new_pixel_color); + } + } + } + + m_editor->active_layer()->did_modify_bitmap(); + m_did_change = true; +} + +ErrorOr LevelsDialog::ensure_reference_bitmap() +{ + if (m_reference_bitmap.is_null()) + m_reference_bitmap = TRY(m_editor->active_layer()->content_bitmap().clone()); + + return {}; +} + +void LevelsDialog::cleanup_resources() +{ + if (m_reference_bitmap) + m_reference_bitmap = nullptr; +} + +void LevelsDialog::generate_precomputed_color_correction() +{ + int delta_brightness = m_brightness_slider->value(); + float contrast_correction_factor = static_cast(259 * (m_contrast_slider->value() + 255) / static_cast(255 * (259 - m_contrast_slider->value()))); + float gamma_correction = 1 / (m_gamma_slider->value() / 100.0); + + for (int color_val = 0; color_val < 256; color_val++) { + m_precomputed_color_correction[color_val] = color_val + delta_brightness; + m_precomputed_color_correction[color_val] = m_precomputed_color_correction[color_val] < 0 ? 0 : m_precomputed_color_correction[color_val]; + m_precomputed_color_correction[color_val] = m_precomputed_color_correction[color_val] > 255 ? 255 : m_precomputed_color_correction[color_val]; + + m_precomputed_color_correction[color_val] = 255 * AK::pow((m_precomputed_color_correction[color_val] / 255.0), gamma_correction); + + m_precomputed_color_correction[color_val] = contrast_correction_factor * (m_precomputed_color_correction[color_val] - 128) + 128; + m_precomputed_color_correction[color_val] = m_precomputed_color_correction[color_val] < 0 ? 0 : m_precomputed_color_correction[color_val]; + m_precomputed_color_correction[color_val] = m_precomputed_color_correction[color_val] > 255 ? 255 : m_precomputed_color_correction[color_val]; + } +} + +} diff --git a/Userland/Applications/PixelPaint/LevelsDialog.gml b/Userland/Applications/PixelPaint/LevelsDialog.gml new file mode 100644 index 0000000000..78b187e7c1 --- /dev/null +++ b/Userland/Applications/PixelPaint/LevelsDialog.gml @@ -0,0 +1,87 @@ +@GUI::Frame { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [4] + } + + @GUI::Widget { + shrink_to_fit: true + layout: @GUI::VerticalBoxLayout {} + + @GUI::Label { + name: "context_label" + enabled: true + fixed_height: 20 + visible: true + text: "Working on Background" + text_alignment: "CenterLeft" + } + + @GUI::Label { + enabled: true + fixed_height: 20 + visible: true + text: "Brightness" + text_alignment: "Left" + } + + @GUI::ValueSlider { + name: "brightness_slider" + value: 0 + max: 255 + min: -255 + page_step: 10 + } + + @GUI::Label { + enabled: true + fixed_height: 20 + visible: true + text: "Contrast" + text_alignment: "Left" + } + + @GUI::ValueSlider { + name: "contrast_slider" + value: 0 + max: 255 + min: -255 + page_step: 10 + } + + @GUI::Label { + enabled: true + fixed_height: 20 + visible: true + text: "Gamma" + text_alignment: "Left" + } + + @GUI::ValueSlider { + name: "gamma_slider" + value: 100 + max: 350 + min: 1 + page_step: 10 + } + + @GUI::HorizontalSeparator {} + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 22 + + @GUI::Layout::Spacer {} + + @GUI::DialogButton { + name: "apply_button" + text: "OK" + } + + @GUI::DialogButton { + name: "cancel_button" + text: "Cancel" + } + } +} diff --git a/Userland/Applications/PixelPaint/LevelsDialog.h b/Userland/Applications/PixelPaint/LevelsDialog.h new file mode 100644 index 0000000000..83245a47ae --- /dev/null +++ b/Userland/Applications/PixelPaint/LevelsDialog.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022, Torsten Engelmann + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "ImageEditor.h" +#include "Layer.h" +#include + +namespace PixelPaint { + +class LevelsDialog final : public GUI::Dialog { + C_OBJECT(LevelsDialog); + +public: + void revert_possible_changes(); + +private: + LevelsDialog(GUI::Window* parent_window, ImageEditor*); + + ImageEditor* m_editor { nullptr }; + RefPtr m_reference_bitmap { nullptr }; + RefPtr m_brightness_slider = { nullptr }; + RefPtr m_contrast_slider = { nullptr }; + RefPtr m_gamma_slider = { nullptr }; + bool m_did_change = false; + int m_precomputed_color_correction[256]; + + ErrorOr ensure_reference_bitmap(); + void generate_new_image(); + void cleanup_resources(); + void generate_precomputed_color_correction(); +}; + +} diff --git a/Userland/Applications/PixelPaint/MainWidget.cpp b/Userland/Applications/PixelPaint/MainWidget.cpp index 6bc6c1ebe3..bea5d9e631 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 "LevelsDialog.h" #include "ResizeImageDialog.h" #include #include @@ -701,6 +702,15 @@ void MainWidget::initialize_menubar(GUI::Window& window) auto& help_menu = window.add_menu("&Help"); help_menu.add_action(GUI::CommonActions::make_about_action("Pixel Paint", GUI::Icon::default_icon("app-pixel-paint"), &window)); + m_levels_dialog_action = GUI::Action::create( + "Change &Levels...", { Mod_Ctrl, Key_L }, g_icon_bag.levels, [&](auto&) { + auto* editor = current_image_editor(); + VERIFY(editor); + auto dialog = PixelPaint::LevelsDialog::construct(&window, editor); + if (dialog->exec() != GUI::Dialog::ExecResult::OK) + dialog->revert_possible_changes(); + }); + auto& toolbar = *find_descendant_of_type_named("toolbar"); toolbar.add_action(*m_new_image_action); toolbar.add_action(*m_open_image_action); @@ -715,6 +725,7 @@ void MainWidget::initialize_menubar(GUI::Window& window) toolbar.add_action(*m_zoom_in_action); toolbar.add_action(*m_zoom_out_action); toolbar.add_action(*m_reset_zoom_action); + m_zoom_combobox = toolbar.add(); m_zoom_combobox->set_max_width(75); m_zoom_combobox->set_model(*GUI::ItemListModel::create(s_suggested_zoom_levels)); @@ -752,6 +763,9 @@ void MainWidget::initialize_menubar(GUI::Window& window) m_zoom_combobox->on_return_pressed = [this]() { m_zoom_combobox->on_change(m_zoom_combobox->text(), GUI::ModelIndex()); }; + + toolbar.add_separator(); + toolbar.add_action(*m_levels_dialog_action); } void MainWidget::set_actions_enabled(bool enabled) diff --git a/Userland/Applications/PixelPaint/MainWidget.h b/Userland/Applications/PixelPaint/MainWidget.h index c817abfa92..5f994b22e9 100644 --- a/Userland/Applications/PixelPaint/MainWidget.h +++ b/Userland/Applications/PixelPaint/MainWidget.h @@ -81,6 +81,7 @@ private: RefPtr m_save_image_action; RefPtr m_save_image_as_action; RefPtr m_close_image_action; + RefPtr m_levels_dialog_action; RefPtr m_cut_action; RefPtr m_copy_action;