/* Camicro - Microscope camera viewer. * * Copyright (C) 2019 Nicolas Schodet * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Contact : * Web: http://ni.fr.eu.org/ * Email: */ #include #include #include "gui_app_window.h" #include "utils.h" struct _GuiAppWindow { GtkApplicationWindow parent; }; typedef struct _GuiAppWindowPrivate GuiAppWindowPrivate; struct _GuiAppWindowPrivate { GuiAppWindow *win; GuiApp *app; GtkDrawingArea *video; GtkButton *start_stop_button; GtkComboBoxText *resolution_combo_box; GtkAdjustment *exposure_adj; GtkAdjustment *gain_adj; GtkCheckButton *wb_button; GtkButton *wb_cal_button; GtkCheckButton *rotate_button; GtkButton *save_button; GdkRectangle allocation; cairo_matrix_t transform; cairo_surface_t *surface; struct image *image; struct image *image_converted; bool white_balance; bool white_balance_calibrated; struct image_white_balance_reference white_balance_reference; bool rotate; int width; int height; struct device *device; bool started; gulong video_ready_handler_id; }; G_DEFINE_TYPE_WITH_PRIVATE(GuiAppWindow, gui_app_window, GTK_TYPE_APPLICATION_WINDOW); /* When input or output size change, recompute transformation matrix. */ static void video_size_update(GuiAppWindowPrivate *priv) { int source_width = priv->width; int source_height = priv->height; int dest_width = priv->allocation.width; int dest_height = priv->allocation.height; if (source_width > 0 && dest_width > 0) { double scale_x = (double) source_width / dest_width; double scale_y = (double) source_height / dest_height; double scale; if (scale_x > scale_y) scale = scale_x; else scale = scale_y; if (scale < 1.0) scale = 1.0; double offset_x = -(dest_width - source_width / scale) / 2.0; double offset_y = -(dest_height - source_height / scale) / 2.0; if (priv->rotate) { cairo_matrix_init_translate(&priv->transform, source_width / 2, source_height / 2); cairo_matrix_rotate(&priv->transform, M_PI); cairo_matrix_translate(&priv->transform, -source_width / 2, -source_height / 2); } else { cairo_matrix_init_identity(&priv->transform); } cairo_matrix_scale(&priv->transform, scale, scale); cairo_matrix_translate(&priv->transform, offset_x, offset_y); } } /* A new image could be available. */ static void video_ready_cb(GuiApp *app, gpointer user_data) { GuiAppWindowPrivate *priv = user_data; if (priv->started) { GError *error = NULL; struct image *image = device_read(priv->device, &error); if (!image) { if (error) { utils_dialog_error(GTK_WINDOW(priv->win), "error reading image: %s", error->message); g_error_free(error); gtk_widget_destroy(GTK_WIDGET(priv->win)); } } else { /* Release old data. */ if (priv->surface) { cairo_surface_destroy(priv->surface); priv->surface = NULL; } if (priv->image) { image_unref(priv->image); priv->image = NULL; } /* White balance. */ if (priv->white_balance && !priv->white_balance_calibrated) { image_white_balance_calibrate(image, &priv->white_balance_reference); priv->white_balance_calibrated = true; } if (priv->white_balance) image_white_balance(image, &priv->white_balance_reference); /* Different format? Convert. */ if (image->format != IMAGE_FORMAT_XBGR32) { if (priv->image_converted && (priv->image_converted->width != image->width || priv->image_converted->height != image->height)) { image_unref(priv->image_converted); priv->image_converted = NULL; } if (!priv->image_converted) { int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, image->width); priv->image_converted = image_new(image->width, image->height, stride, IMAGE_FORMAT_XBGR32); } image_convert(priv->image_converted, image); image_unref(image); image = priv->image_converted; } else { if (priv->image_converted) { image_unref(priv->image_converted); priv->image_converted = NULL; } priv->image = image; } /* Make a surface. */ priv->surface = cairo_image_surface_create_for_data( (unsigned char *) image->pixels, CAIRO_FORMAT_RGB24, image->width, image->height, image->stride); gtk_widget_queue_draw(GTK_WIDGET(priv->video)); gtk_widget_set_sensitive(GTK_WIDGET(priv->save_button), TRUE); } } } /* Start video. */ static void video_start(GuiAppWindowPrivate *priv) { g_assert(!priv->started); int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, priv->width); device_set_resolution(priv->device, priv->width, priv->height, stride); video_size_update(priv); GError *error = NULL; if (!device_start(priv->device, &error)) { utils_dialog_error(GTK_WINDOW(priv->win), "can not start: %s", error->message); g_error_free(error); } else { priv->video_ready_handler_id = g_signal_connect(priv->app, "video-ready", G_CALLBACK(video_ready_cb), priv); gtk_button_set_label(priv->start_stop_button, "Stop"); priv->started = true; } } /* Stop video. */ static void video_stop(GuiAppWindowPrivate *priv) { g_assert(priv->started); if (priv->surface) { cairo_surface_destroy(priv->surface); priv->surface = NULL; } if (priv->image) { image_unref(priv->image); priv->image = NULL; } if (priv->image_converted) { image_unref(priv->image_converted); priv->image_converted = NULL; } g_signal_handler_disconnect(priv->app, priv->video_ready_handler_id); GError *error = NULL; if (!device_stop(priv->device, &error)) { g_warning("device_stop: %s", error->message); g_error_free(error); } priv->started = false; gtk_widget_queue_draw(GTK_WIDGET(priv->video)); gtk_button_set_label(priv->start_stop_button, "Start"); gtk_widget_set_sensitive(GTK_WIDGET(priv->save_button), FALSE); } static gboolean video_draw_cb(GtkWidget *widget, cairo_t *cr, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(widget)); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_paint(cr); if (priv->surface) { cairo_pattern_t *pattern = cairo_pattern_create_for_surface(priv->surface); cairo_pattern_set_matrix(pattern, &priv->transform); cairo_set_source(cr, pattern); cairo_paint(cr); cairo_pattern_destroy(pattern); } return FALSE; } static void video_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(widget)); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); priv->allocation = *allocation; video_size_update(priv); } static void destroy_cb(GtkWidget *widget, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(widget); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); if (priv->started) video_stop(priv); if (priv->device) { GError *error = NULL; if (!device_close(priv->device, &error)) { g_warning("device_close: %s", error->message); g_error_free(error); } priv->device = NULL; } } static void start_stop_button_clicked_cb(GtkButton *button, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); if (!priv->started) video_start(priv); else video_stop(priv); } static void resolution_combo_box_changed_cb(GtkComboBox *combo, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(combo))); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); int sel = gtk_combo_box_get_active(combo); const struct device_info *info = device_get_info(priv->device); g_assert(sel >= 0 && sel < info->resolutions); priv->width = info->resolution[sel].width; priv->height = info->resolution[sel].height; if (priv->started) { video_stop(priv); video_start(priv); } } static void exposure_adj_value_changed_cb(GtkAdjustment *adj, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(user_data); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); double val = gtk_adjustment_get_value(adj); device_set_exposure(priv->device, val); } static void gain_adj_value_changed_cb(GtkAdjustment *adj, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(user_data); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); double val = gtk_adjustment_get_value(adj); device_set_gain(priv->device, val); } static void wb_button_toggled_cb(GtkToggleButton *button, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); priv->white_balance = gtk_toggle_button_get_active(button); gtk_widget_set_sensitive(GTK_WIDGET(priv->wb_cal_button), priv->white_balance); } static void wb_cal_button_clicked_cb(GtkButton *button, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); priv->white_balance_calibrated = false; } static void rotate_button_toggled_cb(GtkToggleButton *button, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); priv->rotate = gtk_toggle_button_get_active(button); video_size_update(priv); } static void save_button_clicked_cb(GtkButton *button, gpointer user_data) { GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); if (priv->surface) { GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(priv->surface, 0, 0, priv->width, priv->height); if (priv->rotate && pixbuf) { GdkPixbuf *rotated = gdk_pixbuf_rotate_simple(pixbuf, 180); g_object_unref(pixbuf); pixbuf = rotated; } if (pixbuf) { GtkWidget *dialog = gtk_file_chooser_dialog_new("Save image", GTK_WINDOW(win), 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); GtkFileFilter *png_filter = gtk_file_filter_new(); gtk_file_filter_set_name(png_filter, "PNG image"); gtk_file_filter_add_mime_type(png_filter, "image/png"); gtk_file_filter_add_pattern(png_filter, "*.png"); gtk_file_chooser_add_filter(chooser, png_filter); GtkFileFilter *jpg_filter = gtk_file_filter_new(); gtk_file_filter_set_name(jpg_filter, "JPEG image"); gtk_file_filter_add_mime_type(jpg_filter, "image/jpeg"); gtk_file_filter_add_pattern(jpg_filter, "*.jpg"); gtk_file_filter_add_pattern(jpg_filter, "*.jpeg"); gtk_file_chooser_add_filter(chooser, jpg_filter); gint res = gtk_dialog_run(GTK_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(chooser); if (filename) { GtkFileFilter *filter = gtk_file_chooser_get_filter(chooser); const char *type = NULL; if (filter == png_filter) type = "png"; else if (filter == jpg_filter) type = "jpeg"; if (type) { GError *error = NULL; if (!gdk_pixbuf_save(pixbuf, filename, type, &error, NULL)) { utils_dialog_error(GTK_WINDOW(win), "can not save image: %s", error->message); g_error_free(error); } } g_free(filename); } } gtk_widget_destroy(dialog); g_object_unref(pixbuf); } } } static void gui_app_window_init(GuiAppWindow *win) { gtk_widget_init_template(GTK_WIDGET(win)); GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); priv->win = win; priv->app = NULL; priv->allocation.width = -1; cairo_matrix_init_identity(&priv->transform); priv->surface = NULL; priv->image = NULL; priv->image_converted = NULL; priv->white_balance = false; priv->white_balance_calibrated = false; priv->rotate = true; priv->width = -1; priv->height = -1; priv->device = NULL; priv->started = false; priv->video_ready_handler_id = 0; } static void gui_app_window_class_init(GuiAppWindowClass *class) { gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(class), "/org/eu/fr/ni/camicro/window.ui"); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, video); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, start_stop_button); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, resolution_combo_box); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, exposure_adj); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, gain_adj); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, wb_button); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, wb_cal_button); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, rotate_button); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class), GuiAppWindow, save_button); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), destroy_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), video_draw_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), video_size_allocate_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), start_stop_button_clicked_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), resolution_combo_box_changed_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), exposure_adj_value_changed_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), gain_adj_value_changed_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), wb_button_toggled_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), wb_cal_button_clicked_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), rotate_button_toggled_cb); gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), save_button_clicked_cb); } GuiAppWindow * gui_app_window_new(GuiApp *app) { return g_object_new(GUI_APP_WINDOW_TYPE, "application", app, NULL); } void gui_app_window_open(GuiAppWindow *win, struct device *device) { GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win); g_assert(!priv->device); priv->app = GUI_APP(gtk_window_get_application(GTK_WINDOW(win))); const struct device_info *info = device_get_info(device); priv->width = info->resolution[0].width; priv->height = info->resolution[0].height; priv->device = device; gtk_adjustment_set_lower(priv->exposure_adj, info->exposure_min_ms); gtk_adjustment_set_upper(priv->exposure_adj, info->exposure_max_ms); gtk_adjustment_set_value(priv->exposure_adj, info->exposure_default_ms); gtk_adjustment_set_lower(priv->gain_adj, info->gain_min); gtk_adjustment_set_upper(priv->gain_adj, info->gain_max); gtk_adjustment_set_value(priv->gain_adj, info->gain_default); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->wb_button), priv->white_balance); gtk_widget_set_sensitive(GTK_WIDGET(priv->wb_cal_button), priv->white_balance); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->rotate_button), priv->rotate); gtk_combo_box_text_remove_all(priv->resolution_combo_box); for (int i = 0; i < info->resolutions; i++) { const struct device_info_resolution *ir = &info->resolution[i]; char buf[32]; int r = snprintf(buf, sizeof(buf), "%dx%d", ir->width, ir->height); g_assert(r < sizeof(buf)); gtk_combo_box_text_append_text(priv->resolution_combo_box, buf); } gtk_combo_box_set_active(GTK_COMBO_BOX(priv->resolution_combo_box), 0); video_start(priv); }