Browse Source

PixelPaint: Add a histogram widget

This adds a simple histogram widget that visualizes the rgb-channels
and brightness for a given image. When hovering over the image it will
indicate what brightness level the pixel at the mouse position has.
Torstennator 3 năm trước cách đây
mục cha
commit
b7e8f32323

+ 1 - 0
Userland/Applications/PixelPaint/CMakeLists.txt

@@ -31,6 +31,7 @@ set(SOURCES
     Filters/LaplaceDiagonal.cpp
     Filters/LaplaceDiagonal.cpp
     Filters/Sepia.cpp
     Filters/Sepia.cpp
     Filters/Sharpen.cpp
     Filters/Sharpen.cpp
+    HistogramWidget.cpp
     IconBag.cpp
     IconBag.cpp
     Image.cpp
     Image.cpp
     ImageEditor.cpp
     ImageEditor.cpp

+ 172 - 0
Userland/Applications/PixelPaint/HistogramWidget.cpp

@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2022, Torsten Engelmann
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "HistogramWidget.h"
+#include "Image.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/Path.h>
+
+REGISTER_WIDGET(PixelPaint, HistogramWidget);
+
+namespace PixelPaint {
+
+HistogramWidget::HistogramWidget()
+{
+    set_height(65);
+}
+
+HistogramWidget::~HistogramWidget()
+{
+    if (m_image)
+        m_image->remove_client(*this);
+}
+
+void HistogramWidget::set_image(Image* image)
+{
+    if (m_image == image)
+        return;
+    if (m_image)
+        m_image->remove_client(*this);
+    m_image = image;
+    if (m_image)
+        m_image->add_client(*this);
+
+    (void)rebuild_histogram_data();
+}
+
+ErrorOr<void> HistogramWidget::rebuild_histogram_data()
+{
+    if (!m_image)
+        return {};
+
+    auto full_bitmap = TRY(m_image->try_compose_bitmap(Gfx::BitmapFormat::BGRA8888));
+
+    m_data.red.clear_with_capacity();
+    m_data.green.clear_with_capacity();
+    m_data.blue.clear_with_capacity();
+    m_data.brightness.clear_with_capacity();
+
+    for (int i = 0; i < 256; i++) {
+        m_data.red.append(0);
+        m_data.green.append(0);
+        m_data.blue.append(0);
+        m_data.brightness.append(0);
+    }
+
+    Color pixel_color;
+    for (int x = 0; x < full_bitmap->width(); x++) {
+        for (int y = 0; y < full_bitmap->height(); y++) {
+            pixel_color = full_bitmap->get_pixel(x, y);
+            if (!pixel_color.alpha())
+                continue;
+
+            m_data.red[pixel_color.red()]++;
+            m_data.green[pixel_color.green()]++;
+            m_data.blue[pixel_color.blue()]++;
+            m_data.brightness[pixel_color.luminosity()]++;
+        }
+    }
+    int max_brightness_frequency = 0;
+    int max_color_frequency = 0;
+    for (int i = 0; i < 256; i++) {
+        if (m_data.red[i] > max_color_frequency)
+            max_color_frequency = m_data.red[i];
+        if (m_data.green[i] > max_color_frequency)
+            max_color_frequency = m_data.green[i];
+        if (m_data.blue[i] > max_color_frequency)
+            max_color_frequency = m_data.blue[i];
+        if (m_data.brightness[i] > max_brightness_frequency)
+            max_brightness_frequency = m_data.brightness[i];
+    }
+
+    // Scale the frequency values to fit the widgets height.
+    m_widget_height = height();
+
+    for (int i = 0; i < 256; i++) {
+        m_data.red[i] = (static_cast<float>(m_data.red[i]) / max_color_frequency) * m_widget_height;
+        m_data.green[i] = (static_cast<float>(m_data.green[i]) / max_color_frequency) * m_widget_height;
+        m_data.blue[i] = (static_cast<float>(m_data.blue[i]) / max_color_frequency) * m_widget_height;
+        m_data.brightness[i] = (static_cast<float>(m_data.brightness[i]) / max_brightness_frequency) * m_widget_height;
+    }
+
+    update();
+    return {};
+}
+
+void HistogramWidget::paint_event(GUI::PaintEvent& event)
+{
+    GUI::Painter painter(*this);
+    painter.add_clip_rect(event.rect());
+
+    if (!m_image)
+        return;
+
+    int bottom_line = m_widget_height - 1;
+    float step_width = static_cast<float>(width()) / 256;
+
+    Gfx::Path brightness_path;
+    Gfx::Path red_channel_path;
+    Gfx::Path green_channel_path;
+    Gfx::Path blue_channel_path;
+    red_channel_path.move_to({ 0, bottom_line - m_data.red[0] });
+    green_channel_path.move_to({ 0, bottom_line - m_data.green[0] });
+    blue_channel_path.move_to({ 0, bottom_line - m_data.blue[0] });
+    brightness_path.move_to({ 0, bottom_line });
+    brightness_path.line_to({ 0, bottom_line });
+
+    float current_x_as_float = 0;
+    int current_x_as_int = 0;
+    int last_drawn_x = -1;
+
+    for (int data_column = 0; data_column < 256; data_column++) {
+        current_x_as_int = static_cast<int>(current_x_as_float);
+        // we would like to skip values that map to the same x position as it does not look so good in the final result
+        if (current_x_as_int == last_drawn_x) {
+            current_x_as_float += step_width;
+            continue;
+        }
+
+        red_channel_path.line_to({ current_x_as_int, bottom_line - m_data.red[data_column] });
+        green_channel_path.line_to({ current_x_as_int, bottom_line - m_data.green[data_column] });
+        blue_channel_path.line_to({ current_x_as_int, bottom_line - m_data.blue[data_column] });
+        brightness_path.line_to({ current_x_as_int, bottom_line - m_data.brightness[data_column] });
+
+        current_x_as_float += step_width;
+        last_drawn_x = current_x_as_int;
+    }
+
+    brightness_path.line_to({ last_drawn_x, bottom_line });
+    brightness_path.close();
+
+    painter.fill_path(brightness_path, Color::MidGray, Gfx::Painter::WindingRule::EvenOdd);
+    painter.stroke_path(red_channel_path, Color(Color::NamedColor::Red).with_alpha(90), 2);
+    painter.stroke_path(green_channel_path, Color(Color::NamedColor::Green).with_alpha(90), 2);
+    painter.stroke_path(blue_channel_path, Color(Color::NamedColor::Blue).with_alpha(90), 2);
+
+    if (m_color_at_mouseposition != Color::Transparent) {
+        int x = m_color_at_mouseposition.luminosity() * step_width;
+        painter.draw_line({ x, 0 }, { x, bottom_line }, Color::from_hsl(45, 1, .7), 1);
+    }
+}
+
+void HistogramWidget::image_changed()
+{
+    (void)rebuild_histogram_data();
+}
+
+void HistogramWidget::set_color_at_mouseposition(Color color)
+{
+    if (m_color_at_mouseposition == color)
+        return;
+
+    m_color_at_mouseposition = color;
+    update();
+}
+
+}

+ 45 - 0
Userland/Applications/PixelPaint/HistogramWidget.h

@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022, Torsten Engelmann
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include "Image.h"
+#include <LibGUI/AbstractScrollableWidget.h>
+
+namespace PixelPaint {
+
+class HistogramWidget final
+    : public GUI::Frame
+    , ImageClient {
+    C_OBJECT(HistogramWidget);
+
+public:
+    virtual ~HistogramWidget() override;
+
+    void set_image(Image*);
+    void image_changed();
+    void set_color_at_mouseposition(Color);
+
+private:
+    HistogramWidget();
+
+    virtual void paint_event(GUI::PaintEvent&) override;
+
+    ErrorOr<void> rebuild_histogram_data();
+    int m_widget_height = 0;
+    Color m_color_at_mouseposition = Color::Transparent;
+    RefPtr<Image> m_image;
+
+    struct HistogramData {
+        Vector<int> red;
+        Vector<int> green;
+        Vector<int> blue;
+        Vector<int> brightness;
+    };
+    HistogramData m_data;
+};
+
+}

+ 8 - 0
Userland/Applications/PixelPaint/MainWidget.cpp

@@ -46,6 +46,7 @@ MainWidget::MainWidget()
 
 
     m_palette_widget = *find_descendant_of_type_named<PixelPaint::PaletteWidget>("palette_widget");
     m_palette_widget = *find_descendant_of_type_named<PixelPaint::PaletteWidget>("palette_widget");
 
 
+    m_histogram_widget = *find_descendant_of_type_named<PixelPaint::HistogramWidget>("histogram_widget");
     m_layer_list_widget = *find_descendant_of_type_named<PixelPaint::LayerListWidget>("layer_list_widget");
     m_layer_list_widget = *find_descendant_of_type_named<PixelPaint::LayerListWidget>("layer_list_widget");
     m_layer_list_widget->on_layer_select = [&](auto* layer) {
     m_layer_list_widget->on_layer_select = [&](auto* layer) {
         auto* editor = current_image_editor();
         auto* editor = current_image_editor();
@@ -73,6 +74,7 @@ MainWidget::MainWidget()
             m_tab_widget->deferred_invoke([&] {
             m_tab_widget->deferred_invoke([&] {
                 m_tab_widget->remove_tab(image_editor);
                 m_tab_widget->remove_tab(image_editor);
                 if (m_tab_widget->children().size() == 0) {
                 if (m_tab_widget->children().size() == 0) {
+                    m_histogram_widget->set_image(nullptr);
                     m_layer_list_widget->set_image(nullptr);
                     m_layer_list_widget->set_image(nullptr);
                     m_layer_properties_widget->set_layer(nullptr);
                     m_layer_properties_widget->set_layer(nullptr);
                     m_palette_widget->set_image_editor(nullptr);
                     m_palette_widget->set_image_editor(nullptr);
@@ -86,11 +88,13 @@ MainWidget::MainWidget()
     m_tab_widget->on_change = [&](auto& widget) {
     m_tab_widget->on_change = [&](auto& widget) {
         auto& image_editor = verify_cast<PixelPaint::ImageEditor>(widget);
         auto& image_editor = verify_cast<PixelPaint::ImageEditor>(widget);
         m_palette_widget->set_image_editor(&image_editor);
         m_palette_widget->set_image_editor(&image_editor);
+        m_histogram_widget->set_image(&image_editor.image());
         m_layer_list_widget->set_image(&image_editor.image());
         m_layer_list_widget->set_image(&image_editor.image());
         m_layer_properties_widget->set_layer(image_editor.active_layer());
         m_layer_properties_widget->set_layer(image_editor.active_layer());
         window()->set_modified(image_editor.is_modified());
         window()->set_modified(image_editor.is_modified());
         image_editor.on_modified_change = [this](bool modified) {
         image_editor.on_modified_change = [this](bool modified) {
             window()->set_modified(modified);
             window()->set_modified(modified);
+            m_histogram_widget->image_changed();
         };
         };
         if (auto* active_tool = m_toolbox->active_tool())
         if (auto* active_tool = m_toolbox->active_tool())
             image_editor.set_active_tool(active_tool);
             image_editor.set_active_tool(active_tool);
@@ -125,6 +129,7 @@ void MainWidget::initialize_menubar(GUI::Window& window)
                 editor.set_title(image_title.is_empty() ? "Untitled" : image_title);
                 editor.set_title(image_title.is_empty() ? "Untitled" : image_title);
                 editor.undo_stack().set_current_unmodified();
                 editor.undo_stack().set_current_unmodified();
 
 
+                m_histogram_widget->set_image(image);
                 m_layer_list_widget->set_image(image);
                 m_layer_list_widget->set_image(image);
                 m_layer_list_widget->set_selected_layer(bg_layer);
                 m_layer_list_widget->set_selected_layer(bg_layer);
             }
             }
@@ -853,13 +858,16 @@ ImageEditor& MainWidget::create_new_editor(NonnullRefPtr<Image> image)
         auto image_rectangle = Gfx::IntRect { 0, 0, image_size.width(), image_size.height() };
         auto image_rectangle = Gfx::IntRect { 0, 0, image_size.width(), image_size.height() };
         if (image_rectangle.contains(mouse_position)) {
         if (image_rectangle.contains(mouse_position)) {
             m_statusbar->set_override_text(mouse_position.to_string());
             m_statusbar->set_override_text(mouse_position.to_string());
+            m_histogram_widget->set_color_at_mouseposition(current_image_editor()->image().color_at(mouse_position));
         } else {
         } else {
             m_statusbar->set_override_text({});
             m_statusbar->set_override_text({});
+            m_histogram_widget->set_color_at_mouseposition(Color::Transparent);
         }
         }
     };
     };
 
 
     image_editor.on_leave = [&]() {
     image_editor.on_leave = [&]() {
         m_statusbar->set_override_text({});
         m_statusbar->set_override_text({});
+        m_histogram_widget->set_color_at_mouseposition(Color::Transparent);
     };
     };
 
 
     image_editor.on_set_guide_visibility = [&](bool show_guides) {
     image_editor.on_set_guide_visibility = [&](bool show_guides) {

+ 2 - 0
Userland/Applications/PixelPaint/MainWidget.h

@@ -7,6 +7,7 @@
 #pragma once
 #pragma once
 
 
 #include "Guide.h"
 #include "Guide.h"
+#include "HistogramWidget.h"
 #include "IconBag.h"
 #include "IconBag.h"
 #include "Image.h"
 #include "Image.h"
 #include "ImageEditor.h"
 #include "ImageEditor.h"
@@ -58,6 +59,7 @@ private:
 
 
     RefPtr<ToolboxWidget> m_toolbox;
     RefPtr<ToolboxWidget> m_toolbox;
     RefPtr<PaletteWidget> m_palette_widget;
     RefPtr<PaletteWidget> m_palette_widget;
+    RefPtr<HistogramWidget> m_histogram_widget;
     RefPtr<LayerListWidget> m_layer_list_widget;
     RefPtr<LayerListWidget> m_layer_list_widget;
     RefPtr<LayerPropertiesWidget> m_layer_properties_widget;
     RefPtr<LayerPropertiesWidget> m_layer_properties_widget;
     RefPtr<ToolPropertiesWidget> m_tool_properties_widget;
     RefPtr<ToolPropertiesWidget> m_tool_properties_widget;

+ 13 - 0
Userland/Applications/PixelPaint/PixelPaintWindow.gml

@@ -61,6 +61,19 @@
                 max_height: 94
                 max_height: 94
             }
             }
 
 
+            @GUI::GroupBox {
+                title: "Histogram"
+                max_height: 90
+                layout: @GUI::VerticalBoxLayout {
+                    margins: [6]
+                }
+
+                @PixelPaint::HistogramWidget {
+                    name: "histogram_widget"
+                    max_height: 65
+                }
+            }
+
             @PixelPaint::ToolPropertiesWidget {
             @PixelPaint::ToolPropertiesWidget {
                 name: "tool_properties_widget"
                 name: "tool_properties_widget"
                 max_height: 144
                 max_height: 144