summaryrefslogtreecommitdiff
path: root/Userland
diff options
context:
space:
mode:
authorTorstennator <engelTorsten@gmx.de>2022-04-18 15:24:49 +0200
committerLinus Groh <mail@linusgroh.de>2022-04-21 17:26:52 +0200
commitb7e8f32323c9fd8bc1fcd2058e717d4307fa8f1f (patch)
treeb90df39215899e18ef4d80f6d961b70582b4581b /Userland
parent5702f016f008109c8807ed0be4d93e1fa12be938 (diff)
downloadserenity-b7e8f32323c9fd8bc1fcd2058e717d4307fa8f1f.zip
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.
Diffstat (limited to 'Userland')
-rw-r--r--Userland/Applications/PixelPaint/CMakeLists.txt1
-rw-r--r--Userland/Applications/PixelPaint/HistogramWidget.cpp172
-rw-r--r--Userland/Applications/PixelPaint/HistogramWidget.h45
-rw-r--r--Userland/Applications/PixelPaint/MainWidget.cpp8
-rw-r--r--Userland/Applications/PixelPaint/MainWidget.h2
-rw-r--r--Userland/Applications/PixelPaint/PixelPaintWindow.gml13
6 files changed, 241 insertions, 0 deletions
diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt
index 65aeef7394..e8c1f5f7e1 100644
--- a/Userland/Applications/PixelPaint/CMakeLists.txt
+++ b/Userland/Applications/PixelPaint/CMakeLists.txt
@@ -31,6 +31,7 @@ set(SOURCES
Filters/LaplaceDiagonal.cpp
Filters/Sepia.cpp
Filters/Sharpen.cpp
+ HistogramWidget.cpp
IconBag.cpp
Image.cpp
ImageEditor.cpp
diff --git a/Userland/Applications/PixelPaint/HistogramWidget.cpp b/Userland/Applications/PixelPaint/HistogramWidget.cpp
new file mode 100644
index 0000000000..d84cd3cf36
--- /dev/null
+++ b/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();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/HistogramWidget.h b/Userland/Applications/PixelPaint/HistogramWidget.h
new file mode 100644
index 0000000000..72d0c1cff5
--- /dev/null
+++ b/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;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/MainWidget.cpp b/Userland/Applications/PixelPaint/MainWidget.cpp
index 625e7df559..27c52473c1 100644
--- a/Userland/Applications/PixelPaint/MainWidget.cpp
+++ b/Userland/Applications/PixelPaint/MainWidget.cpp
@@ -46,6 +46,7 @@ MainWidget::MainWidget()
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->on_layer_select = [&](auto* layer) {
auto* editor = current_image_editor();
@@ -73,6 +74,7 @@ MainWidget::MainWidget()
m_tab_widget->deferred_invoke([&] {
m_tab_widget->remove_tab(image_editor);
if (m_tab_widget->children().size() == 0) {
+ m_histogram_widget->set_image(nullptr);
m_layer_list_widget->set_image(nullptr);
m_layer_properties_widget->set_layer(nullptr);
m_palette_widget->set_image_editor(nullptr);
@@ -86,11 +88,13 @@ MainWidget::MainWidget()
m_tab_widget->on_change = [&](auto& widget) {
auto& image_editor = verify_cast<PixelPaint::ImageEditor>(widget);
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_properties_widget->set_layer(image_editor.active_layer());
window()->set_modified(image_editor.is_modified());
image_editor.on_modified_change = [this](bool modified) {
window()->set_modified(modified);
+ m_histogram_widget->image_changed();
};
if (auto* active_tool = m_toolbox->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.undo_stack().set_current_unmodified();
+ m_histogram_widget->set_image(image);
m_layer_list_widget->set_image(image);
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() };
if (image_rectangle.contains(mouse_position)) {
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 {
m_statusbar->set_override_text({});
+ m_histogram_widget->set_color_at_mouseposition(Color::Transparent);
}
};
image_editor.on_leave = [&]() {
m_statusbar->set_override_text({});
+ m_histogram_widget->set_color_at_mouseposition(Color::Transparent);
};
image_editor.on_set_guide_visibility = [&](bool show_guides) {
diff --git a/Userland/Applications/PixelPaint/MainWidget.h b/Userland/Applications/PixelPaint/MainWidget.h
index 440b977cf0..c817abfa92 100644
--- a/Userland/Applications/PixelPaint/MainWidget.h
+++ b/Userland/Applications/PixelPaint/MainWidget.h
@@ -7,6 +7,7 @@
#pragma once
#include "Guide.h"
+#include "HistogramWidget.h"
#include "IconBag.h"
#include "Image.h"
#include "ImageEditor.h"
@@ -58,6 +59,7 @@ private:
RefPtr<ToolboxWidget> m_toolbox;
RefPtr<PaletteWidget> m_palette_widget;
+ RefPtr<HistogramWidget> m_histogram_widget;
RefPtr<LayerListWidget> m_layer_list_widget;
RefPtr<LayerPropertiesWidget> m_layer_properties_widget;
RefPtr<ToolPropertiesWidget> m_tool_properties_widget;
diff --git a/Userland/Applications/PixelPaint/PixelPaintWindow.gml b/Userland/Applications/PixelPaint/PixelPaintWindow.gml
index e7d429de9e..59e721b07e 100644
--- a/Userland/Applications/PixelPaint/PixelPaintWindow.gml
+++ b/Userland/Applications/PixelPaint/PixelPaintWindow.gml
@@ -61,6 +61,19 @@
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 {
name: "tool_properties_widget"
max_height: 144