/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Pix
*
* Copyright (C) 2011-2014 Free Software Foundation, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include
#include
#include
#include
#include "gth-file-tool-adjust-contrast.h"
#include "gth-preview-tool.h"
#define GET_WIDGET(x) (_gtk_builder_get_widget (self->priv->builder, (x)))
#define APPLY_DELAY 150
#define PREVIEW_SIZE 0.9
#define HISTOGRAM_CROP_0_5 0.005 /* ignores the 0.5% on each side of the histogram */
#define HISTOGRAM_CROP_1_5 0.015 /* ignores the 1.5% on each side of the histogram */
typedef enum {
METHOD_STRETCH,
METHOD_STRETCH_0_5,
METHOD_STRETCH_1_5,
METHOD_EQUALIZE_LINEAR,
METHOD_EQUALIZE_SQUARE_ROOT
} Method;
struct _GthFileToolAdjustContrastPrivate {
cairo_surface_t *destination;
cairo_surface_t *preview;
GtkBuilder *builder;
GthTask *image_task;
GthImageViewerTool *preview_tool;
guint apply_event;
gboolean apply_to_original;
gboolean closing;
Method method;
Method last_applied_method;
gboolean view_original;
};
G_DEFINE_TYPE_WITH_CODE (GthFileToolAdjustContrast,
gth_file_tool_adjust_contrast,
GTH_TYPE_IMAGE_VIEWER_PAGE_TOOL,
G_ADD_PRIVATE (GthFileToolAdjustContrast))
/* equalize histogram */
typedef struct {
Method method;
long **value_map;
} EqualizeData;
static void
value_map_free (long **value_map)
{
int c;
if (value_map == NULL)
return;
for (c = 0; c < GTH_HISTOGRAM_N_CHANNELS; c++)
g_free (value_map[c]);
g_free (value_map);
}
static double
get_histogram_value (GthHistogram *histogram,
GthHistogramChannel channel,
int bin,
Method method)
{
double h = gth_histogram_get_value (histogram, channel, bin);
switch (method) {
case METHOD_EQUALIZE_SQUARE_ROOT:
return (h >= 2) ? sqrt (h) : h;
case METHOD_EQUALIZE_LINEAR:
return h;
default:
g_assert_not_reached ();
}
return 0;
}
static long **
get_value_map_for_equalize (GthHistogram *histogram,
Method method)
{
long **value_map;
int c, v;
value_map = g_new (long *, GTH_HISTOGRAM_N_CHANNELS);
for (c = 0; c < GTH_HISTOGRAM_N_CHANNELS; c++) {
double sum;
double scale;
sum = 0.0;
for (v = 0; v < 255; v++)
sum += 2 * get_histogram_value (histogram, c, v, method);
sum += get_histogram_value (histogram, c, 255, method);
scale = 255 / sum;
value_map[c] = g_new (long, 256);
value_map[c][0] = 0;
sum = get_histogram_value (histogram, c, 0, method);
for (v = 1; v < 255; v++) {
double delta = get_histogram_value (histogram, c, v, method);
sum += delta;
value_map[c][v] = (int) round (sum * scale);
sum += delta;
}
value_map[c][255] = 255;
}
return value_map;
}
static long **
get_value_map_for_stretch (GthHistogram *histogram,
Method method)
{
long **value_map;
int n_pixels, lower_limit, higher_limit;
int c, v;
n_pixels = gth_histogram_get_n_pixels (histogram);
switch (method) {
case METHOD_STRETCH:
lower_limit = 0;
higher_limit = n_pixels;
break;
case METHOD_STRETCH_0_5:
lower_limit = n_pixels * HISTOGRAM_CROP_0_5;
higher_limit = n_pixels * (1 - HISTOGRAM_CROP_0_5);
break;
case METHOD_STRETCH_1_5:
lower_limit = n_pixels * HISTOGRAM_CROP_1_5;
higher_limit = n_pixels * (1 - HISTOGRAM_CROP_1_5);
break;
default:
g_assert_not_reached ();
}
value_map = g_new (long *, GTH_HISTOGRAM_N_CHANNELS);
for (c = 0; c < GTH_HISTOGRAM_N_CHANNELS; c++) {
guchar min, max;
double sum, scale;
min = 0;
sum = 0;
for (v = 0; v < 256; v++) {
sum += gth_histogram_get_value (histogram, c, v);
if (sum >= lower_limit) {
min = v;
break;
}
}
max = 0;
sum = 0;
for (v = 0; v < 256; v++) {
sum += gth_histogram_get_value (histogram, c, v);
if (sum <= higher_limit)
max = v;
}
scale = 255.0 / (max - min);
value_map[c] = g_new (long, 256);
for (v = 0; v <= min; v++)
value_map[c][v] = 0;
for (v = min + 1; v < max; v++)
value_map[c][v] = (int) round (scale * (v - min));
for (v = max; v <= 255; v++)
value_map[c][v] = 255;
}
return value_map;
}
static void
adjust_contrast_setup (EqualizeData *equalize_data,
cairo_surface_t *source)
{
GthHistogram *histogram;
histogram = gth_histogram_new ();
gth_histogram_calculate_for_image (histogram, source);
switch (equalize_data->method) {
case METHOD_STRETCH:
case METHOD_STRETCH_0_5:
case METHOD_STRETCH_1_5:
equalize_data->value_map = get_value_map_for_stretch (histogram, equalize_data->method);
break;
case METHOD_EQUALIZE_LINEAR:
case METHOD_EQUALIZE_SQUARE_ROOT:
equalize_data->value_map = get_value_map_for_equalize (histogram, equalize_data->method);
break;
}
g_object_unref (histogram);
}
static inline guchar
adjust_contrast_func (EqualizeData *equalize_data,
int n_channel,
guchar value)
{
return (guchar) equalize_data->value_map[n_channel][value];
}
static gpointer
adjust_contrast_exec (GthAsyncTask *task,
gpointer user_data)
{
EqualizeData *equalize_data = user_data;
cairo_surface_t *source;
cairo_format_t format;
int width;
int height;
int source_stride;
cairo_surface_t *destination;
int destination_stride;
unsigned char *p_source_line;
unsigned char *p_destination_line;
unsigned char *p_source;
unsigned char *p_destination;
gboolean cancelled;
double progress;
int x, y, temp;
unsigned char red, green, blue, alpha;
/* initialize the extra data */
source = gth_image_task_get_source_surface (GTH_IMAGE_TASK (task));
adjust_contrast_setup (equalize_data, source);
/* convert the image */
format = cairo_image_surface_get_format (source);
width = cairo_image_surface_get_width (source);
height = cairo_image_surface_get_height (source);
source_stride = cairo_image_surface_get_stride (source);
destination = cairo_image_surface_create (format, width, height);
destination_stride = cairo_image_surface_get_stride (destination);
p_source_line = _cairo_image_surface_flush_and_get_data (source);
p_destination_line = _cairo_image_surface_flush_and_get_data (destination);
for (y = 0; y < height; y++) {
gth_async_task_get_data (task, NULL, &cancelled, NULL);
if (cancelled) {
cairo_surface_destroy (destination);
cairo_surface_destroy (source);
return NULL;
}
progress = (double) y / height;
gth_async_task_set_data (task, NULL, NULL, &progress);
p_source = p_source_line;
p_destination = p_destination_line;
for (x = 0; x < width; x++) {
CAIRO_GET_RGBA (p_source, red, green, blue, alpha);
red = adjust_contrast_func (equalize_data, GTH_HISTOGRAM_CHANNEL_RED, red);
green = adjust_contrast_func (equalize_data, GTH_HISTOGRAM_CHANNEL_GREEN, green);
blue = adjust_contrast_func (equalize_data, GTH_HISTOGRAM_CHANNEL_BLUE, blue);
CAIRO_SET_RGBA (p_destination, red, green, blue, alpha);
p_source += 4;
p_destination += 4;
}
p_source_line += source_stride;
p_destination_line += destination_stride;
}
cairo_surface_mark_dirty (destination);
gth_image_task_set_destination_surface (GTH_IMAGE_TASK (task), destination);
cairo_surface_destroy (destination);
cairo_surface_destroy (source);
return NULL;
}
static void
adjust_contrast_data_destroy (gpointer user_data)
{
EqualizeData *equalize_data = user_data;
if (equalize_data->value_map != NULL)
value_map_free (equalize_data->value_map);
g_free (equalize_data);
}
static void apply_changes (GthFileToolAdjustContrast *self);
static void
image_task_completed_cb (GthTask *task,
GError *error,
gpointer user_data)
{
GthFileToolAdjustContrast *self = user_data;
GthImage *destination_image;
self->priv->image_task = NULL;
if (self->priv->closing) {
g_object_unref (task);
gth_image_viewer_page_tool_reset_image (GTH_IMAGE_VIEWER_PAGE_TOOL (self));
return;
}
if (error != NULL) {
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
apply_changes (self);
g_object_unref (task);
return;
}
destination_image = gth_image_task_get_destination (GTH_IMAGE_TASK (task));
if (destination_image == NULL) {
g_object_unref (task);
return;
}
cairo_surface_destroy (self->priv->destination);
self->priv->destination = gth_image_get_cairo_surface (destination_image);
self->priv->last_applied_method = self->priv->method;
if (self->priv->apply_to_original) {
if (self->priv->destination != NULL) {
GtkWidget *window;
GthViewerPage *viewer_page;
window = gth_file_tool_get_window (GTH_FILE_TOOL (self));
viewer_page = gth_browser_get_viewer_page (GTH_BROWSER (window));
gth_image_viewer_page_set_image (GTH_IMAGE_VIEWER_PAGE (viewer_page), self->priv->destination, TRUE);
}
gth_file_tool_hide_options (GTH_FILE_TOOL (self));
}
else {
if (! self->priv->view_original)
gth_preview_tool_set_image (GTH_PREVIEW_TOOL (self->priv->preview_tool), self->priv->destination);
}
g_object_unref (task);
}
static GthTask *
get_image_task_for_method (Method method)
{
EqualizeData *equalize_data;
equalize_data = g_new (EqualizeData, 1);
equalize_data->method = method;
equalize_data->value_map = NULL;
return gth_image_task_new (_("Applying changes"),
NULL,
adjust_contrast_exec,
NULL,
equalize_data,
adjust_contrast_data_destroy);
}
static gboolean
apply_cb (gpointer user_data)
{
GthFileToolAdjustContrast *self = user_data;
GtkWidget *window;
if (self->priv->apply_event != 0) {
g_source_remove (self->priv->apply_event);
self->priv->apply_event = 0;
}
if (self->priv->image_task != NULL) {
gth_task_cancel (self->priv->image_task);
return FALSE;
}
window = gth_file_tool_get_window (GTH_FILE_TOOL (self));
self->priv->image_task = get_image_task_for_method (self->priv->method);
if (self->priv->apply_to_original)
gth_image_task_set_source_surface (GTH_IMAGE_TASK (self->priv->image_task), gth_image_viewer_page_tool_get_source (GTH_IMAGE_VIEWER_PAGE_TOOL (self)));
else
gth_image_task_set_source_surface (GTH_IMAGE_TASK (self->priv->image_task), self->priv->preview);
g_signal_connect (self->priv->image_task,
"completed",
G_CALLBACK (image_task_completed_cb),
self);
gth_browser_exec_task (GTH_BROWSER (window), self->priv->image_task, GTH_TASK_FLAGS_DEFAULT);
return FALSE;
}
static void
apply_changes (GthFileToolAdjustContrast *self)
{
if (self->priv->apply_event != 0) {
g_source_remove (self->priv->apply_event);
self->priv->apply_event = 0;
}
self->priv->apply_event = g_timeout_add (APPLY_DELAY, apply_cb, self);
}
static void
gth_file_tool_adjust_contrast_reset_image (GthImageViewerPageTool *base)
{
GthFileToolAdjustContrast *self = GTH_FILE_TOOL_ADJUST_CONTRAST (base);
if (self->priv->image_task != NULL) {
self->priv->closing = TRUE;
return;
}
if (self->priv->apply_event != 0) {
g_source_remove (self->priv->apply_event);
self->priv->apply_event = 0;
}
gth_image_viewer_page_reset (GTH_IMAGE_VIEWER_PAGE (gth_image_viewer_page_tool_get_page (GTH_IMAGE_VIEWER_PAGE_TOOL (self))));
gth_file_tool_hide_options (GTH_FILE_TOOL (self));
}
static void
filter_grid_activated_cb (GthFilterGrid *filter_grid,
int filter_id,
gpointer user_data)
{
GthFileToolAdjustContrast *self = user_data;
self->priv->view_original = (filter_id == GTH_FILTER_GRID_NO_FILTER);
if (self->priv->view_original) {
gth_preview_tool_set_image (GTH_PREVIEW_TOOL (self->priv->preview_tool), self->priv->preview);
}
else if (filter_id == self->priv->last_applied_method) {
gth_preview_tool_set_image (GTH_PREVIEW_TOOL (self->priv->preview_tool), self->priv->destination);
}
else {
self->priv->method = filter_id;
apply_changes (self);
}
}
static GtkWidget *
gth_file_tool_adjust_contrast_get_options (GthFileTool *base)
{
GthFileToolAdjustContrast *self;
GtkWidget *window;
GthViewerPage *viewer_page;
GtkWidget *viewer;
cairo_surface_t *source;
GtkWidget *options;
int width, height;
GtkAllocation allocation;
GtkWidget *filter_grid;
self = (GthFileToolAdjustContrast *) base;
window = gth_file_tool_get_window (base);
viewer_page = gth_browser_get_viewer_page (GTH_BROWSER (window));
if (! GTH_IS_IMAGE_VIEWER_PAGE (viewer_page))
return NULL;
_cairo_clear_surface (&self->priv->preview);
_cairo_clear_surface (&self->priv->destination);
viewer = gth_image_viewer_page_get_image_viewer (GTH_IMAGE_VIEWER_PAGE (viewer_page));
source = gth_image_viewer_page_tool_get_source (GTH_IMAGE_VIEWER_PAGE_TOOL (self));
if (source == NULL)
return NULL;
width = cairo_image_surface_get_width (source);
height = cairo_image_surface_get_height (source);
gtk_widget_get_allocation (GTK_WIDGET (viewer), &allocation);
if (scale_keeping_ratio (&width, &height, PREVIEW_SIZE * allocation.width, PREVIEW_SIZE * allocation.height, FALSE))
self->priv->preview = _cairo_image_surface_scale_fast (source, width, height);
else
self->priv->preview = cairo_surface_reference (source);
self->priv->destination = cairo_surface_reference (self->priv->preview);
self->priv->apply_to_original = FALSE;
self->priv->closing = FALSE;
self->priv->builder = _gtk_builder_new_from_file ("adjust-contrast-options.ui", "file_tools");
options = _gtk_builder_get_widget (self->priv->builder, "options");
gtk_widget_show (options);
filter_grid = gth_filter_grid_new ();
gth_filter_grid_add_filter (GTH_FILTER_GRID (filter_grid),
METHOD_STRETCH_0_5,
get_image_task_for_method (METHOD_STRETCH_0_5),
_("Stretch"),
/* xgettext:no-c-format */
_("Stretch the histogram after trimming 0.5% from both ends"));
gth_filter_grid_add_filter (GTH_FILTER_GRID (filter_grid),
METHOD_EQUALIZE_SQUARE_ROOT,
get_image_task_for_method (METHOD_EQUALIZE_SQUARE_ROOT),
_("Equalize"),
_("Equalize the histogram using the square root function"));
gth_filter_grid_add_filter (GTH_FILTER_GRID (filter_grid),
METHOD_EQUALIZE_LINEAR,
get_image_task_for_method (METHOD_EQUALIZE_LINEAR),
_("Uniform"),
_("Equalize the histogram using the linear function"));
g_signal_connect (filter_grid,
"activated",
G_CALLBACK (filter_grid_activated_cb),
self);
gtk_widget_show (filter_grid);
gtk_box_pack_start (GTK_BOX (GET_WIDGET ("filter_grid_box")), filter_grid, TRUE, FALSE, 0);
self->priv->preview_tool = gth_preview_tool_new ();
gth_preview_tool_set_image (GTH_PREVIEW_TOOL (self->priv->preview_tool), self->priv->preview);
gth_image_viewer_set_tool (GTH_IMAGE_VIEWER (viewer), self->priv->preview_tool);
gth_filter_grid_activate (GTH_FILTER_GRID (filter_grid), METHOD_STRETCH_0_5);
gth_filter_grid_generate_previews (GTH_FILTER_GRID (filter_grid), source);
return options;
}
static void
gth_file_tool_adjust_contrast_destroy_options (GthFileTool *base)
{
GthFileToolAdjustContrast *self;
GtkWidget *window;
GthViewerPage *viewer_page;
self = (GthFileToolAdjustContrast *) base;
if (self->priv->apply_event != 0) {
g_source_remove (self->priv->apply_event);
self->priv->apply_event = 0;
}
window = gth_file_tool_get_window (GTH_FILE_TOOL (self));
viewer_page = gth_browser_get_viewer_page (GTH_BROWSER (window));
gth_image_viewer_page_reset_viewer_tool (GTH_IMAGE_VIEWER_PAGE (viewer_page));
gth_viewer_page_update_sensitivity (viewer_page);
_g_clear_object (&self->priv->builder);
_cairo_clear_surface (&self->priv->preview);
_cairo_clear_surface (&self->priv->destination);
self->priv->method = GTH_FILTER_GRID_NO_FILTER;
self->priv->last_applied_method = GTH_FILTER_GRID_NO_FILTER;
self->priv->view_original = TRUE;
}
static void
gth_file_tool_adjust_contrast_apply_options (GthFileTool *base)
{
GthFileToolAdjustContrast *self;
self = (GthFileToolAdjustContrast *) base;
if (! self->priv->view_original) {
self->priv->apply_to_original = TRUE;
apply_changes (self);
}
}
static void
gth_file_tool_adjust_contrast_finalize (GObject *object)
{
GthFileToolAdjustContrast *self;
g_return_if_fail (object != NULL);
g_return_if_fail (GTH_IS_FILE_TOOL_ADJUST_CONTRAST (object));
self = (GthFileToolAdjustContrast *) object;
_g_clear_object (&self->priv->builder);
_cairo_clear_surface (&self->priv->preview);
_cairo_clear_surface (&self->priv->destination);
G_OBJECT_CLASS (gth_file_tool_adjust_contrast_parent_class)->finalize (object);
}
static void
gth_file_tool_adjust_contrast_class_init (GthFileToolAdjustContrastClass *klass)
{
GObjectClass *gobject_class;
GthFileToolClass *file_tool_class;
GthImageViewerPageToolClass *image_viewer_page_tool_class;
gobject_class = (GObjectClass*) klass;
gobject_class->finalize = gth_file_tool_adjust_contrast_finalize;
file_tool_class = GTH_FILE_TOOL_CLASS (klass);
file_tool_class->get_options = gth_file_tool_adjust_contrast_get_options;
file_tool_class->destroy_options = gth_file_tool_adjust_contrast_destroy_options;
file_tool_class->apply_options = gth_file_tool_adjust_contrast_apply_options;
image_viewer_page_tool_class = (GthImageViewerPageToolClass *) klass;
image_viewer_page_tool_class->reset_image = gth_file_tool_adjust_contrast_reset_image;
}
static void
gth_file_tool_adjust_contrast_init (GthFileToolAdjustContrast *self)
{
self->priv = gth_file_tool_adjust_contrast_get_instance_private (self);
self->priv->preview = NULL;
self->priv->destination = NULL;
self->priv->builder = NULL;
self->priv->method = GTH_FILTER_GRID_NO_FILTER;
self->priv->last_applied_method = GTH_FILTER_GRID_NO_FILTER;
self->priv->view_original = TRUE;
gth_file_tool_construct (GTH_FILE_TOOL (self),
"image-adjust-contrast-symbolic",
_("Adjust Contrast"),
GTH_TOOLBOX_SECTION_COLORS);
gtk_widget_set_tooltip_text (GTK_WIDGET (self), _("Automatic contrast adjustment"));
}