512 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			512 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
//
 | 
						||
// fiv-jpegcrop.c: lossless JPEG cropper
 | 
						||
//
 | 
						||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
						||
//
 | 
						||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
						||
// purpose with or without fee is hereby granted.
 | 
						||
//
 | 
						||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
						||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
						||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
						||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
						||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
						||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
						||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
						||
//
 | 
						||
 | 
						||
#include <gtk/gtk.h>
 | 
						||
#include <turbojpeg.h>
 | 
						||
 | 
						||
#include <stdlib.h>
 | 
						||
#include <string.h>
 | 
						||
 | 
						||
#include "config.h"
 | 
						||
 | 
						||
// --- Utilities ---------------------------------------------------------------
 | 
						||
 | 
						||
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
 | 
						||
 | 
						||
static void
 | 
						||
exit_fatal(const char *format, ...)
 | 
						||
{
 | 
						||
	va_list ap;
 | 
						||
	va_start(ap, format);
 | 
						||
 | 
						||
	gchar *format_nl = g_strdup_printf("%s\n", format);
 | 
						||
	vfprintf(stderr, format_nl, ap);
 | 
						||
	free(format_nl);
 | 
						||
 | 
						||
	va_end(ap);
 | 
						||
	exit(EXIT_FAILURE);
 | 
						||
}
 | 
						||
 | 
						||
// --- Main --------------------------------------------------------------------
 | 
						||
 | 
						||
struct {
 | 
						||
	GFile *location;
 | 
						||
	gchar *data;
 | 
						||
	gsize len;
 | 
						||
	int width, height, subsampling, colorspace;
 | 
						||
	int mcu_width, mcu_height;
 | 
						||
	cairo_surface_t *surface;
 | 
						||
 | 
						||
	int top, left, right, bottom;
 | 
						||
 | 
						||
	GtkWidget *label;
 | 
						||
	GtkWidget *window;
 | 
						||
	GtkWidget *scrolled;
 | 
						||
	GtkWidget *view;
 | 
						||
} g;
 | 
						||
 | 
						||
static void
 | 
						||
show_error_dialog(GError *error)
 | 
						||
{
 | 
						||
	GtkWidget *dialog =
 | 
						||
		gtk_message_dialog_new(GTK_WINDOW(g.window), GTK_DIALOG_MODAL,
 | 
						||
			GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
 | 
						||
	gtk_dialog_run(GTK_DIALOG(dialog));
 | 
						||
	gtk_widget_destroy(dialog);
 | 
						||
	g_error_free(error);
 | 
						||
}
 | 
						||
 | 
						||
static gboolean
 | 
						||
on_draw(G_GNUC_UNUSED GtkWidget *widget, cairo_t *cr,
 | 
						||
	G_GNUC_UNUSED gpointer user_data)
 | 
						||
{
 | 
						||
	cairo_set_source_surface(cr, g.surface, 1, 1);
 | 
						||
	cairo_paint(cr);
 | 
						||
 | 
						||
	cairo_rectangle(cr,
 | 
						||
		1 + g.left - 0.5,
 | 
						||
		1 + g.top - 0.5,
 | 
						||
		g.right - g.left + 1,
 | 
						||
		g.bottom - g.top + 1);
 | 
						||
	cairo_set_source_rgb(cr, 1, 1, 1);
 | 
						||
	cairo_set_line_width(cr, 1);
 | 
						||
	cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);
 | 
						||
	cairo_stroke(cr);
 | 
						||
 | 
						||
	cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
 | 
						||
	cairo_rectangle(cr, 1, 1, g.width, g.height);
 | 
						||
	cairo_rectangle(
 | 
						||
		cr, g.left, g.top, g.right - g.left + 2, g.bottom - g.top + 2);
 | 
						||
	cairo_clip(cr);
 | 
						||
	cairo_set_source_rgba(cr, 0, 0, 0, 0.5);
 | 
						||
	cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
 | 
						||
	cairo_paint(cr);
 | 
						||
	return TRUE;
 | 
						||
}
 | 
						||
 | 
						||
static GFile *
 | 
						||
run_chooser(GtkWidget *dialog)
 | 
						||
{
 | 
						||
	GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
 | 
						||
	gtk_file_chooser_set_local_only(chooser, FALSE);
 | 
						||
 | 
						||
	GtkFileFilter *jpeg = gtk_file_filter_new();
 | 
						||
	gtk_file_filter_add_mime_type(jpeg, "image/jpeg");
 | 
						||
	gtk_file_filter_add_pattern(jpeg, "*.jpg");
 | 
						||
	gtk_file_filter_add_pattern(jpeg, "*.jpeg");
 | 
						||
	gtk_file_filter_add_pattern(jpeg, "*.jpe");
 | 
						||
	gtk_file_filter_set_name(jpeg, "JPEG");
 | 
						||
	gtk_file_chooser_add_filter(chooser, jpeg);
 | 
						||
 | 
						||
	GtkFileFilter *all = gtk_file_filter_new();
 | 
						||
	gtk_file_filter_add_pattern(all, "*");
 | 
						||
	gtk_file_filter_set_name(all, "All files");
 | 
						||
	gtk_file_chooser_add_filter(chooser, all);
 | 
						||
 | 
						||
	GFile *file = NULL;
 | 
						||
	switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
 | 
						||
	default:
 | 
						||
		gtk_widget_destroy(dialog);
 | 
						||
		// Fall-through.
 | 
						||
	case GTK_RESPONSE_NONE:
 | 
						||
		return file;
 | 
						||
	case GTK_RESPONSE_ACCEPT:
 | 
						||
		file = gtk_file_chooser_get_file(chooser);
 | 
						||
		gtk_widget_destroy(dialog);
 | 
						||
		return file;
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
static GFile *
 | 
						||
choose_file_to_open(void)
 | 
						||
{
 | 
						||
	GtkWidget *dialog = gtk_file_chooser_dialog_new("Open image",
 | 
						||
		NULL, GTK_FILE_CHOOSER_ACTION_OPEN,
 | 
						||
		"_Cancel", GTK_RESPONSE_CANCEL,
 | 
						||
		"_Open", GTK_RESPONSE_ACCEPT, NULL);
 | 
						||
	return run_chooser(dialog);
 | 
						||
}
 | 
						||
 | 
						||
static GFile *
 | 
						||
choose_file_to_save(void)
 | 
						||
{
 | 
						||
	GtkWidget *dialog = gtk_file_chooser_dialog_new("Saved cropped image as",
 | 
						||
		GTK_WINDOW(g.window), GTK_FILE_CHOOSER_ACTION_SAVE,
 | 
						||
		"_Cancel", GTK_RESPONSE_CANCEL,
 | 
						||
		"_Save", GTK_RESPONSE_ACCEPT, NULL);
 | 
						||
 | 
						||
	GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
 | 
						||
	gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
 | 
						||
	(void) gtk_file_chooser_set_file(chooser, g.location, NULL);
 | 
						||
	return run_chooser(dialog);
 | 
						||
}
 | 
						||
 | 
						||
static void
 | 
						||
on_save_as(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
 | 
						||
{
 | 
						||
	tjhandle h = tjInitTransform();
 | 
						||
	if (!h) {
 | 
						||
		show_error_dialog(g_error_new_literal(
 | 
						||
			G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
 | 
						||
		return;
 | 
						||
	}
 | 
						||
 | 
						||
	// Convert up front, because the target is in memory.
 | 
						||
	tjtransform t = {
 | 
						||
		.r.x = g.left,
 | 
						||
		.r.y = g.top,
 | 
						||
		.r.w = g.right - g.left,
 | 
						||
		.r.h = g.bottom - g.top,
 | 
						||
		.op = TJXOP_NONE,
 | 
						||
		.options = TJXOPT_CROP | TJXOPT_PROGRESSIVE | TJXOPT_PERFECT,
 | 
						||
	};
 | 
						||
 | 
						||
	guchar *data = NULL;
 | 
						||
	gulong len = 0;
 | 
						||
	if (tjTransform(h, (const guchar *) g.data, g.len, 1, &data, &len, &t, 0)) {
 | 
						||
		show_error_dialog(g_error_new_literal(
 | 
						||
			G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
 | 
						||
		goto out;
 | 
						||
	}
 | 
						||
 | 
						||
	GFile *file = choose_file_to_save();
 | 
						||
	GError *error = NULL;
 | 
						||
	if (file &&
 | 
						||
		!g_file_replace_contents(file, (const char *) data, len, NULL, FALSE,
 | 
						||
			G_FILE_CREATE_NONE, NULL, NULL, &error)) {
 | 
						||
		show_error_dialog(error);
 | 
						||
		goto out;
 | 
						||
	}
 | 
						||
 | 
						||
	g_clear_object(&file);
 | 
						||
	tjFree(data);
 | 
						||
out:
 | 
						||
	if (tjDestroy(h)) {
 | 
						||
		show_error_dialog(g_error_new_literal(
 | 
						||
			G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
static void
 | 
						||
update_label(void)
 | 
						||
{
 | 
						||
	gchar *text = g_strdup_printf("(%d, %d) × (%d, %d)", g.left, g.top,
 | 
						||
		g.right - g.left, g.bottom - g.top);
 | 
						||
	gtk_label_set_label(GTK_LABEL(g.label), text);
 | 
						||
	g_free(text);
 | 
						||
}
 | 
						||
 | 
						||
static void
 | 
						||
update(void)
 | 
						||
{
 | 
						||
	update_label();
 | 
						||
	gtk_widget_queue_draw(g.view);
 | 
						||
}
 | 
						||
 | 
						||
static void
 | 
						||
on_reset(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
 | 
						||
{
 | 
						||
	g.top = 0;
 | 
						||
	g.left = 0;
 | 
						||
	g.right = g.width;
 | 
						||
	g.bottom = g.height;
 | 
						||
	update();
 | 
						||
}
 | 
						||
 | 
						||
static gboolean
 | 
						||
on_mouse(guint state, guint button, gdouble x, gdouble y)
 | 
						||
{
 | 
						||
	if (state != 0)
 | 
						||
		return FALSE;
 | 
						||
 | 
						||
	switch (button) {
 | 
						||
	case GDK_BUTTON_PRIMARY:
 | 
						||
		g.left = CLAMP((int) (x - 1), 0, g.right) / g.mcu_width * g.mcu_width;
 | 
						||
		g.top = CLAMP((int) (y - 1), 0, g.bottom) / g.mcu_height * g.mcu_height;
 | 
						||
		update();
 | 
						||
		return TRUE;
 | 
						||
	case GDK_BUTTON_SECONDARY:
 | 
						||
		// Inclusive of pointer position.
 | 
						||
		g.right = CLAMP(x, g.left, g.width);
 | 
						||
		g.bottom = CLAMP(y, g.top, g.height);
 | 
						||
		update();
 | 
						||
		return TRUE;
 | 
						||
	default:
 | 
						||
		return FALSE;
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
static gboolean
 | 
						||
on_press(G_GNUC_UNUSED GtkWidget *self, GdkEventButton *event,
 | 
						||
	G_GNUC_UNUSED gpointer user_data)
 | 
						||
{
 | 
						||
	return on_mouse(event->state, event->button, event->x, event->y);
 | 
						||
}
 | 
						||
 | 
						||
static gboolean
 | 
						||
on_motion(G_GNUC_UNUSED GtkWidget *self, GdkEventMotion *event,
 | 
						||
	G_GNUC_UNUSED gpointer user_data)
 | 
						||
{
 | 
						||
	switch (event->state) {
 | 
						||
	case GDK_BUTTON1_MASK:
 | 
						||
		return on_mouse(0, GDK_BUTTON_PRIMARY, event->x, event->y);
 | 
						||
	case GDK_BUTTON3_MASK:
 | 
						||
		return on_mouse(0, GDK_BUTTON_SECONDARY, event->x, event->y);
 | 
						||
	}
 | 
						||
	return FALSE;
 | 
						||
}
 | 
						||
 | 
						||
static void
 | 
						||
on_drag_begin(
 | 
						||
	GtkGestureDrag *self, gdouble start_x, gdouble start_y, gpointer user_data)
 | 
						||
{
 | 
						||
	// The middle mouse button will never be triggered by touch screens,
 | 
						||
	// so there is only the NULL sequence to care about.
 | 
						||
	gtk_gesture_set_state(GTK_GESTURE(self), GTK_EVENT_SEQUENCE_CLAIMED);
 | 
						||
 | 
						||
	GdkWindow *window = gtk_widget_get_window(g.view);
 | 
						||
	GdkCursor *cursor =
 | 
						||
		gdk_cursor_new_from_name(gdk_window_get_display(window), "grabbing");
 | 
						||
	gdk_window_set_cursor(window, cursor);
 | 
						||
	g_object_unref(cursor);
 | 
						||
 | 
						||
	double *last = user_data;
 | 
						||
	last[0] = start_x;
 | 
						||
	last[1] = start_y;
 | 
						||
}
 | 
						||
 | 
						||
static void
 | 
						||
on_drag_update(GtkGestureDrag *self, gdouble offset_x, gdouble offset_y,
 | 
						||
	gpointer user_data)
 | 
						||
{
 | 
						||
	double start_x = 0, start_y = 0;
 | 
						||
	gtk_gesture_drag_get_start_point(self, &start_x, &start_y);
 | 
						||
 | 
						||
	double *last = user_data,
 | 
						||
		diff_x = (start_x + offset_x) - last[0],
 | 
						||
		diff_y = (start_y + offset_y) - last[1];
 | 
						||
 | 
						||
	last[0] = start_x + offset_x;
 | 
						||
	last[1] = start_y + offset_y;
 | 
						||
 | 
						||
	GtkScrolledWindow *sw = GTK_SCROLLED_WINDOW(g.scrolled);
 | 
						||
	GtkAdjustment *h = gtk_scrolled_window_get_hadjustment(sw);
 | 
						||
	GtkAdjustment *v = gtk_scrolled_window_get_vadjustment(sw);
 | 
						||
	if (diff_x)
 | 
						||
		gtk_adjustment_set_value(h, gtk_adjustment_get_value(h) - diff_x);
 | 
						||
	if (diff_y)
 | 
						||
		gtk_adjustment_set_value(v, gtk_adjustment_get_value(v) - diff_y);
 | 
						||
}
 | 
						||
 | 
						||
static void
 | 
						||
on_drag_end(G_GNUC_UNUSED GtkGestureDrag *self, G_GNUC_UNUSED gdouble start_x,
 | 
						||
	G_GNUC_UNUSED gdouble start_y, G_GNUC_UNUSED gpointer user_data)
 | 
						||
{
 | 
						||
	// Cursors follow the widget hierarchy.
 | 
						||
	gdk_window_set_cursor(gtk_widget_get_window(g.view), NULL);
 | 
						||
}
 | 
						||
 | 
						||
static gboolean
 | 
						||
open_jpeg(const char *data, gsize len, GError **error)
 | 
						||
{
 | 
						||
	tjhandle h = tjInitDecompress();
 | 
						||
	if (!h) {
 | 
						||
		g_set_error_literal(
 | 
						||
			error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
 | 
						||
		return FALSE;
 | 
						||
	}
 | 
						||
 | 
						||
	if (tjDecompressHeader3(h, (const guint8 *) data, len, &g.width, &g.height,
 | 
						||
			&g.subsampling, &g.colorspace)) {
 | 
						||
		g_set_error_literal(
 | 
						||
			error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
 | 
						||
		tjDestroy(h);
 | 
						||
		return FALSE;
 | 
						||
	}
 | 
						||
 | 
						||
	g.top = 0;
 | 
						||
	g.left = 0;
 | 
						||
	g.right = g.width;
 | 
						||
	g.bottom = g.height;
 | 
						||
 | 
						||
	g.mcu_width = tjMCUWidth[g.subsampling];
 | 
						||
	g.mcu_height = tjMCUHeight[g.subsampling];
 | 
						||
 | 
						||
	if (tjDestroy(h)) {
 | 
						||
		g_set_error_literal(
 | 
						||
			error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
 | 
						||
		return FALSE;
 | 
						||
	}
 | 
						||
 | 
						||
	// TODO(p): Eventually, convert to using fiv-io.c directly,
 | 
						||
	// which will pull in most of fiv's dependencies,
 | 
						||
	// but also enable correct color management, even for CMYK.
 | 
						||
	// NOTE: It's possible to include this as a mode of the main binary.
 | 
						||
	GInputStream *is = g_memory_input_stream_new_from_data(data, len, NULL);
 | 
						||
	GdkPixbuf *pixbuf = gdk_pixbuf_new_from_stream(is, NULL, error);
 | 
						||
	g_object_unref(is);
 | 
						||
	if (!pixbuf)
 | 
						||
		return FALSE;
 | 
						||
 | 
						||
	const char *orientation = gdk_pixbuf_get_option(pixbuf, "orientation");
 | 
						||
	if (orientation && strlen(orientation) == 1) {
 | 
						||
		int n = *orientation - '0';
 | 
						||
		if (n >= 1 && n <= 8) {
 | 
						||
			// TODO(p): Apply this to the view, somehow.
 | 
						||
		}
 | 
						||
	}
 | 
						||
 | 
						||
	g.surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 1, NULL);
 | 
						||
	cairo_status_t surface_status = cairo_surface_status(g.surface);
 | 
						||
	if (surface_status != CAIRO_STATUS_SUCCESS) {
 | 
						||
		g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
 | 
						||
			cairo_status_to_string(surface_status));
 | 
						||
		g_clear_pointer(&g.surface, cairo_surface_destroy);
 | 
						||
		g_object_unref(pixbuf);
 | 
						||
		return FALSE;
 | 
						||
	}
 | 
						||
	return TRUE;
 | 
						||
}
 | 
						||
 | 
						||
int
 | 
						||
main(int argc, char *argv[])
 | 
						||
{
 | 
						||
	gboolean show_version = FALSE;
 | 
						||
	gchar **args = NULL;
 | 
						||
 | 
						||
	const GOptionEntry options[] = {
 | 
						||
		{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
 | 
						||
			NULL, "[FILE | URI]"},
 | 
						||
		{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
 | 
						||
			&show_version, "Output version information and exit", NULL},
 | 
						||
		{},
 | 
						||
	};
 | 
						||
 | 
						||
	GError *error = NULL;
 | 
						||
	gboolean initialized = gtk_init_with_args(
 | 
						||
		&argc, &argv, " - Lossless JPEG cropper", options, NULL, &error);
 | 
						||
	if (show_version) {
 | 
						||
		const char *version = PROJECT_VERSION;
 | 
						||
		printf("%s %s\n", "fiv-jpegcrop", &version[*version == 'v']);
 | 
						||
		return 0;
 | 
						||
	}
 | 
						||
	if (!initialized)
 | 
						||
		exit_fatal("%s", error->message);
 | 
						||
 | 
						||
	gtk_window_set_default_icon_name(PROJECT_NAME);
 | 
						||
 | 
						||
	// TODO(p): Rather use G_OPTION_ARG_CALLBACK with G_OPTION_FLAG_FILENAME.
 | 
						||
	// Alternatively, GOptionContext with gtk_get_option_group(TRUE).
 | 
						||
	// Then we can show the help string here instead (in fiv as well).
 | 
						||
	if (args && args[1])
 | 
						||
		exit_fatal("Too many arguments");
 | 
						||
	else if (args)
 | 
						||
		g.location = g_file_new_for_commandline_arg(args[0]);
 | 
						||
	else if (!(g.location = choose_file_to_open()))
 | 
						||
		exit(EXIT_SUCCESS);
 | 
						||
 | 
						||
	g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 | 
						||
	g_signal_connect(g.window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
 | 
						||
 | 
						||
	GFileInfo *info = g_file_query_info(g.location,
 | 
						||
		G_FILE_ATTRIBUTE_STANDARD_NAME
 | 
						||
		"," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
 | 
						||
		G_FILE_QUERY_INFO_NONE, NULL, &error);
 | 
						||
	if (!info ||
 | 
						||
		!g_file_load_contents(
 | 
						||
			g.location, NULL, &g.data, &g.len, NULL, &error) ||
 | 
						||
		!open_jpeg(g.data, g.len, &error)) {
 | 
						||
		show_error_dialog(error);
 | 
						||
		exit(EXIT_FAILURE);
 | 
						||
	}
 | 
						||
 | 
						||
	GtkWidget *header = gtk_header_bar_new();
 | 
						||
	gtk_window_set_titlebar(GTK_WINDOW(g.window), header);
 | 
						||
	gtk_header_bar_set_title(
 | 
						||
		GTK_HEADER_BAR(header), g_file_info_get_display_name(info));
 | 
						||
	gtk_header_bar_set_subtitle(GTK_HEADER_BAR(header),
 | 
						||
		"Use L/R mouse buttons to adjust the crop region.");
 | 
						||
	gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(header), TRUE);
 | 
						||
 | 
						||
	g.label = gtk_label_new(NULL);
 | 
						||
	gtk_header_bar_pack_start(GTK_HEADER_BAR(header), g.label);
 | 
						||
	update_label();
 | 
						||
 | 
						||
	GtkWidget *save = gtk_button_new_from_icon_name(
 | 
						||
		"document-save-as-symbolic", GTK_ICON_SIZE_BUTTON);
 | 
						||
	gtk_widget_set_tooltip_text(save, "Save as...");
 | 
						||
	g_signal_connect(save, "clicked", G_CALLBACK(on_save_as), NULL);
 | 
						||
	gtk_header_bar_pack_end(GTK_HEADER_BAR(header), save);
 | 
						||
 | 
						||
	GtkWidget *reset = gtk_button_new_with_mnemonic("_Reset");
 | 
						||
	gtk_widget_set_tooltip_text(reset, "Reset the crop region");
 | 
						||
	g_signal_connect(reset, "clicked", G_CALLBACK(on_reset), NULL);
 | 
						||
	gtk_header_bar_pack_end(GTK_HEADER_BAR(header), reset);
 | 
						||
 | 
						||
	g.view = gtk_drawing_area_new();
 | 
						||
	gtk_widget_set_size_request(g.view, g.width + 2, g.height + 2);
 | 
						||
	gtk_widget_add_events(g.view,
 | 
						||
		GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
 | 
						||
			GDK_POINTER_MOTION_MASK);
 | 
						||
	g_signal_connect(g.view, "draw",
 | 
						||
		G_CALLBACK(on_draw), NULL);
 | 
						||
	g_signal_connect(g.view, "button-press-event",
 | 
						||
		G_CALLBACK(on_press), NULL);
 | 
						||
	g_signal_connect(g.view, "motion-notify-event",
 | 
						||
		G_CALLBACK(on_motion), NULL);
 | 
						||
 | 
						||
	g.scrolled = gtk_scrolled_window_new(NULL, NULL);
 | 
						||
	gtk_scrolled_window_set_overlay_scrolling(
 | 
						||
		GTK_SCROLLED_WINDOW(g.scrolled), FALSE);
 | 
						||
	gtk_scrolled_window_set_propagate_natural_width(
 | 
						||
		GTK_SCROLLED_WINDOW(g.scrolled), TRUE);
 | 
						||
	gtk_scrolled_window_set_propagate_natural_height(
 | 
						||
		GTK_SCROLLED_WINDOW(g.scrolled), TRUE);
 | 
						||
 | 
						||
	GtkGesture *drag = gtk_gesture_drag_new(g.scrolled);
 | 
						||
	gtk_event_controller_set_propagation_phase(
 | 
						||
		GTK_EVENT_CONTROLLER(drag), GTK_PHASE_CAPTURE);
 | 
						||
	gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_MIDDLE);
 | 
						||
 | 
						||
	double last_drag_point[2] = {};
 | 
						||
	g_signal_connect(drag, "drag-begin",
 | 
						||
		G_CALLBACK(on_drag_begin), last_drag_point);
 | 
						||
	g_signal_connect(drag, "drag-update",
 | 
						||
		G_CALLBACK(on_drag_update), last_drag_point);
 | 
						||
	g_signal_connect(drag, "drag-end",
 | 
						||
		G_CALLBACK(on_drag_end), last_drag_point);
 | 
						||
 | 
						||
	gtk_container_add(GTK_CONTAINER(g.scrolled), g.view);
 | 
						||
	gtk_container_add(GTK_CONTAINER(g.window), g.scrolled);
 | 
						||
	gtk_window_set_default_size(GTK_WINDOW(g.window), 800, 600);
 | 
						||
	gtk_widget_show_all(g.window);
 | 
						||
 | 
						||
	// It probably needs to be realized.
 | 
						||
	GdkWindow *window = gtk_widget_get_window(g.scrolled);
 | 
						||
	GdkCursor *cursor =
 | 
						||
		gdk_cursor_new_from_name(gdk_window_get_display(window), "crosshair");
 | 
						||
	gdk_window_set_cursor(window, cursor);
 | 
						||
	g_object_unref(cursor);
 | 
						||
 | 
						||
	gtk_main();
 | 
						||
 | 
						||
	g_free(g.data);
 | 
						||
	g_object_unref(g.location);
 | 
						||
	g_object_unref(info);
 | 
						||
	return 0;
 | 
						||
}
 |