diff options
Diffstat (limited to '')
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | TODO | 42 | ||||
| -rw-r--r-- | collection.c | 364 | ||||
| -rw-r--r-- | common.h | 67 | ||||
| -rw-r--r-- | config.mk | 4 | ||||
| -rw-r--r-- | dragging.c | 69 | ||||
| -rw-r--r-- | dropping.c | 65 | ||||
| -rw-r--r-- | events.c | 37 | ||||
| -rw-r--r-- | files.c | 46 | ||||
| -rw-r--r-- | gbild.c | 189 | ||||
| -rw-r--r-- | image.c | 70 | ||||
| -rw-r--r-- | opening.c | 49 | ||||
| -rw-r--r-- | preferences.c | 223 | ||||
| -rw-r--r-- | printing.c | 95 | ||||
| -rw-r--r-- | properties.c | 159 | ||||
| -rw-r--r-- | render.c | 118 | ||||
| -rw-r--r-- | transform.c | 145 | ||||
| -rw-r--r-- | ui.c | 71 | ||||
| -rw-r--r-- | zoom.c | 31 |
19 files changed, 1470 insertions, 377 deletions
@@ -4,6 +4,7 @@ CONFIGFILE = config.mk include $(CONFIGFILE) OBJ =\ + collection.o\ dragging.o\ dropping.o\ events.o\ @@ -11,8 +12,10 @@ OBJ =\ filters.o\ image.o\ opening.o\ + preferences.o\ printing.o\ properties.o\ + render.o\ transform.o\ ui.o\ zoom.o\ @@ -1,11 +1,10 @@ -Optional (opt-in) side-pane ("_Property pane") with image properties (accelerator: F9) +Use hand cursor when dragging image to move it -Optional (opt-in) bottom-pane ("Image _collection") with image thumbnails (accelerator: Control+F9) - The bottom-pane shall support: - * drag and drop to reorder - * drag to another application to drop the file by path there - * drop form another application to add the image to the collection - * right-clicking on a thumbnail and "_Remove from collection" (icon: edit-delete) +Properties shall display SVG data + +Render icon of image when also when dragging from the image collection + +Save preferences to file Automatically go to next picture @@ -13,18 +12,23 @@ Animation support "Properties" shall display Exif thumbnail if embedded -Preferences for transparency - -Preferences for changling GDK_INTERP_BILINEAR - -Preferences for scaling to physical size rather than 1:1 pixel mapping - -Perference for automatically applying embedded transformation ("orientation") - -Add support for farbfeld +Colour space support -Add support for lss16 +Add support for bicubic interpolation -Add support for pam (portable anymap) +Add support for Lanczos resampling -Colour space support +Add command line options for settings and panes: + +f = no fullscreen + -p = show property pane + +p = hide property pane + -c = show image collection + +c = hide image collection + -t = apply embedded orientation + +t = do not embedded orientation + -n = apply normal size is physical size + +n = do not apply normal size is physical size + -s NAME = apply scaling algorithm NAME + -S SIZE = apply chequer size SIZE, 0 for no chequered background + -C COLOUR1[:COLOUR2] = use COLOUR1 and COLOUR2 (= COLOUR1 if omitted) for the chequered background + +C = do not apply chequered background diff --git a/collection.c b/collection.c new file mode 100644 index 0000000..c39f80b --- /dev/null +++ b/collection.c @@ -0,0 +1,364 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +GtkWidget *collection_view; + +static GtkWidget *collection_pane; +static GtkCellRenderer *collection_renderer; +static GtkListStore *collection_model; +static int collection_height = -1; +static size_t popup_file; + +static char url_list_name[] = "text/uri-list"; +static GtkTargetEntry url_list = {url_list_name, 0u, 0u}; + +enum { + COLLECTION_THUMBNAIL, + COLLECTION_INDEX, + COLLECTION_COLUMNS +}; + +static void +free_paths(GList *paths) +{ + GList *path; + for (path = paths; path; path = path->next) + gtk_tree_path_free(path->data); + g_list_free(paths); +} + +static int +collection_thumbnail_size(void) +{ + return MAX(1, collection_view->allocation.height - 16); +} + +static GdkPixbuf * +create_thumbnail(struct file *file, int maximum_size) +{ + GdkPixbuf *thumbnail = NULL; + int width = file->image.width; + int height = file->image.height; + double w = (double)width; + double h = (double)height; + double s = (double)maximum_size / MAX(w, h); + s = MIN(1.0, s); + width = (int)(s * w); + height = (int)(s * h); + width = MAX(1, width); + height = MAX(1, height); + if (file->image.vector) { + thumbnail = rasterise(file->image.vector, width, height); + if (thumbnail) + return thumbnail; + } + if (file->image.raster) + return gdk_pixbuf_scale_simple(file->image.raster, width, height, GDK_INTERP_BILINEAR); + return NULL; +} + +static void +populate_collection(void) +{ + GtkTreeIter iter; + GdkPixbuf *thumbnail; + size_t i; + int thumbnail_size = collection_thumbnail_size(); + int item_size = thumbnail_size + 2 * gtk_icon_view_get_item_padding(GTK_ICON_VIEW(collection_view)); + gtk_icon_view_set_columns(GTK_ICON_VIEW(collection_view), nfiles ? (int)nfiles : 1); + gtk_icon_view_set_item_width(GTK_ICON_VIEW(collection_view), item_size); + gtk_cell_renderer_set_fixed_size(collection_renderer, thumbnail_size, thumbnail_size); + gtk_list_store_clear(collection_model); + for (i = 0u; i < nfiles; i++) { + thumbnail = create_thumbnail(&files[i], thumbnail_size); + if (!thumbnail) + continue; + gtk_list_store_append(collection_model, &iter); + gtk_list_store_set(collection_model, &iter, COLLECTION_THUMBNAIL, + thumbnail, COLLECTION_INDEX, (unsigned long)i, -1); + g_object_unref(thumbnail); + } +} + +static void +select_current_file(void) +{ + GtkTreeIter iter; + unsigned long index; + GtkTreePath *path; + if (!nfiles || !gtk_tree_model_get_iter_first(GTK_TREE_MODEL(collection_model), &iter)) + return; + do { + gtk_tree_model_get(GTK_TREE_MODEL(collection_model), &iter, COLLECTION_INDEX, &index, -1); + if ((size_t)index == current_file) { + path = gtk_tree_model_get_path(GTK_TREE_MODEL(collection_model), &iter); + gtk_icon_view_select_path(GTK_ICON_VIEW(collection_view), path); + gtk_icon_view_scroll_to_path(GTK_ICON_VIEW(collection_view), path, 1, 0.5, 0.5); + gtk_tree_path_free(path); + return; + } + } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(collection_model), &iter)); +} + +static int +get_selected_index(size_t *index) +{ + GList *paths; + GtkTreeIter iter; + unsigned long value; + int found = 0; + paths = gtk_icon_view_get_selected_items(GTK_ICON_VIEW(collection_view)); + if (!paths) + return 0; + if (gtk_tree_model_get_iter(GTK_TREE_MODEL(collection_model), &iter, paths->data)) { + gtk_tree_model_get(GTK_TREE_MODEL(collection_model), &iter, COLLECTION_INDEX, &value, -1); + *index = (size_t)value; + found = 1; + } + free_paths(paths); + return found; +} + +static void +collection_size_allocated(GtkWidget *widget, GtkAllocation *allocation, void *user) +{ + (void) widget; + (void) user; + if (allocation->height == collection_height) + return; + collection_height = allocation->height; + populate_collection(); + select_current_file(); +} + +static int +collection_index_at(int x, int y, size_t *index) +{ + GtkTreeIter iter; + unsigned long value; + int found = 0; + GtkTreePath *path = gtk_icon_view_get_path_at_pos(GTK_ICON_VIEW(collection_view), x, y); + if (!path) + return 0; + if (gtk_tree_model_get_iter(GTK_TREE_MODEL(collection_model), &iter, path)) { + gtk_tree_model_get(GTK_TREE_MODEL(collection_model), &iter, COLLECTION_INDEX, &value, -1); + *index = (size_t)value; + found = 1; + } + gtk_tree_path_free(path); + return found; +} + +void +navigate_collection_drag(void) +{ + size_t index; + if (get_selected_index(&index) && navigate_without_redraw(index, 0u)) + navigate_redraw(); +} + +static void +collection_selection_changed(GtkIconView *view, void *user) +{ + (void) view; + (void) user; + navigate_collection_drag(); +} + +static void +reorder_file(size_t source, size_t target) +{ + struct file moved; + if (target >= nfiles) + target = nfiles - 1u; + if (!nfiles || source >= nfiles || source == target) + return; + moved = files[source]; + if (source < target) { + memmove(&files[source], &files[source + 1u], (target - source) * sizeof(*files)); + if (current_file == source) + current_file = target; + else if (current_file > source && current_file <= target) + current_file -= 1u; + } else { + memmove(&files[target + 1u], &files[target], (source - target) * sizeof(*files)); + if (current_file == source) + current_file = target; + else if (current_file >= target && current_file < source) + current_file += 1u; + } + files[target] = moved; + collection_files_changed(); + navigate_redraw(); +} + +static void +drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data, + unsigned int info, unsigned int time, void *user) +{ + GError *error = NULL; + size_t index; + char *uri; + (void) widget; + (void) context; + (void) info; + (void) time; + (void) user; + if (!get_selected_index(&index) || index >= nfiles || !files[index].path) + return; + uri = g_filename_to_uri(files[index].path, NULL, &error); + if (!uri) { + weprintf("g_filename_to_uri: %s", error->message); + g_error_free(error); + return; + } + gtk_selection_data_set_uris(data, (char *[]){uri, NULL}); + g_free(uri); +} + +static void +drag_data_received(GtkWidget *widget, GdkDragContext *context, int x, int y, + GtkSelectionData *data, unsigned int info, unsigned int time, void *user) +{ + struct file *dropped_files; + char **uris; + size_t source_index, n; + GtkTreePath *path = NULL; + GtkWidget *source = gtk_drag_get_source_widget(context); + size_t target; + (void) info; + (void) user; + if (source == collection_view) { + if (get_selected_index(&source_index)) + goto reorder; + } else if (source == display_area) { + if (source == display_area) + source_index = current_file; + reorder: + if (gtk_icon_view_get_dest_item_at_pos(GTK_ICON_VIEW(widget), x, y, &path, NULL)) + target = (size_t)gtk_tree_path_get_indices(path)[0]; + else + target = nfiles; + if (path) + gtk_tree_path_free(path); + reorder_file(source_index, target); + } else { + uris = gtk_selection_data_get_uris(data); + n = get_dropped_files(&dropped_files, uris); + if (n) { + add_images(dropped_files, files_from_array(dropped_files, n)); + free(dropped_files); + } + if (uris) + g_strfreev(uris); + } + gtk_drag_finish(context, 1, 0, time); +} + +static void +remove_from_collection(GtkMenuItem *item, void *user) +{ + struct file removed; + (void) item; + (void) user; + if (popup_file >= nfiles) + return; + removed = files[popup_file]; + if (popup_file + 1u < nfiles) + memmove(&files[popup_file], &files[popup_file + 1u], (nfiles - (popup_file + 1u)) * sizeof(*files)); + nfiles -= 1u; + destroy_file(&removed); + forget_transformed_raster(); + if (current_file > popup_file) + current_file -= 1u; + else if (current_file >= nfiles) + current_file = nfiles ? nfiles - 1u : 0u; + collection_files_changed(); + navigate_redraw(); + set_actions_sensitive(); +} + +static int +collection_button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) +{ + GtkWidget *menu, *item; + size_t index; + (void) widget; + (void) user; + if (event->button != 3u || !collection_index_at((int)event->x, (int)event->y, &index)) + return 0; + popup_file = index; + menu = gtk_menu_new(); + item = gtk_image_menu_item_new_from_stock(GTK_STOCK_DELETE, NULL); + gtk_menu_item_set_label(GTK_MENU_ITEM(item), "_Remove from collection"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(&remove_from_collection), NULL); + gtk_widget_show_all(menu); + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time); + return 1; +} + +GtkWidget * +create_collection_pane(void) +{ + collection_pane = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(collection_pane), GTK_POLICY_ALWAYS, GTK_POLICY_NEVER); + gtk_widget_set_size_request(collection_pane, -1, INITIAL_COLLECTION_PANE_HEIGHT); + gtk_widget_set_no_show_all(collection_pane, 1); + + collection_model = gtk_list_store_new(COLLECTION_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_ULONG); + collection_view = gtk_icon_view_new_with_model(GTK_TREE_MODEL(collection_model)); + gtk_icon_view_set_selection_mode(GTK_ICON_VIEW(collection_view), GTK_SELECTION_SINGLE); + gtk_icon_view_set_orientation(GTK_ICON_VIEW(collection_view), GTK_ORIENTATION_HORIZONTAL); + + collection_renderer = gtk_cell_renderer_pixbuf_new(); + g_object_set(collection_renderer, "xalign", 0.5, "yalign", 0.5, NULL); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(collection_view), collection_renderer, 0); + gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(collection_view), collection_renderer, "pixbuf", COLLECTION_THUMBNAIL); + gtk_container_add(GTK_CONTAINER(collection_pane), collection_view); + + gtk_widget_add_events(collection_view, GDK_BUTTON_PRESS_MASK); + gtk_drag_source_set(collection_view, GDK_BUTTON1_MASK, &url_list, 1u, GDK_ACTION_COPY | GDK_ACTION_MOVE); + gtk_drag_dest_set(collection_view, GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP, + &url_list, 1u, GDK_ACTION_COPY | GDK_ACTION_MOVE); + + g_signal_connect(collection_view, "size-allocate", G_CALLBACK(&collection_size_allocated), NULL); + g_signal_connect(collection_view, "selection-changed", G_CALLBACK(&collection_selection_changed), NULL); + g_signal_connect(collection_view, "button-press-event", G_CALLBACK(&collection_button_pressed), NULL); + g_signal_connect(collection_view, "drag-data-get", G_CALLBACK(&drag_data_get), NULL); + g_signal_connect(collection_view, "drag-data-received", G_CALLBACK(&drag_data_received), NULL); + + return collection_pane; +} + +void +set_collection_pane_visible(int visible) +{ + if (visible) { + gtk_widget_show_all(collection_view); + gtk_widget_show(collection_pane); + populate_collection(); + select_current_file(); + gtk_widget_grab_focus(collection_view); + } else { + gtk_widget_hide(collection_pane); + gtk_widget_grab_focus(display_area); + } + g_idle_add(&recalculate_zoom_mode, NULL); +} + +void +collection_files_changed(void) +{ + if (collection_pane && gtk_widget_get_visible(collection_pane)) { + populate_collection(); + select_current_file(); + } +} + +void +update_collection(void) +{ + if (collection_pane && gtk_widget_get_visible(collection_pane)) + select_current_file(); +} @@ -11,6 +11,12 @@ #ifndef PREVIEW_HEIGHT # define PREVIEW_HEIGHT 256 #endif +#ifndef INITIAL_COLLECTION_PANE_HEIGHT +# define INITIAL_COLLECTION_PANE_HEIGHT 160 +#endif +#ifndef INITIAL_PROPERTY_PANE_HEIGHT +# define INITIAL_PROPERTY_PANE_HEIGHT 300 +#endif #if defined(__GNUC__) # pragma GCC diagnostic push @@ -20,6 +26,7 @@ #include <gdk/gdkkeysyms.h> #include <gexiv2/gexiv2.h> #include <gtk/gtk.h> +#include <librsvg/rsvg.h> #include <libsimple.h> #include <libsimple-arg.h> #include <math.h> @@ -77,10 +84,23 @@ enum transformation { TRANS_NEGATIVE = 0x08 }; +struct image { + GdkPixbuf *raster; + RsvgHandle *vector; + int width; + int height; + int has_x_dpi; + int has_y_dpi; + int has_transformer; + cairo_matrix_t transformer; + double x_dpi; + double y_dpi; +}; + struct file { char *path; off_t size; - GdkPixbuf *image; + struct image image; char **exif_tags; GExiv2Metadata *exif; }; @@ -88,10 +108,19 @@ struct file { typedef double Zoom; /* TODO */ +/* collection.c */ +extern GtkWidget *collection_view; +GtkWidget *create_collection_pane(void); +void set_collection_pane_visible(int visible); +void navigate_collection_drag(void); +void collection_files_changed(void); +void update_collection(void); + /* dragging.c */ void set_up_dragging(void); /* dropping.c */ +size_t get_dropped_files(struct file **files_out, char **uris); void set_up_dropping(void); /* events.c */ @@ -99,6 +128,9 @@ void open_action(GtkAction *action, void *user); void reload_action(GtkAction *action, void *user); void print_action(GtkAction *action, void *user); void properties_action(GtkAction *action, void *user); +void property_pane_action(GtkToggleAction *action, void *user); +void collection_pane_action(GtkToggleAction *action, void *user); +void preferences_action(GtkAction *action, void *user); void close_action(GtkAction *action, void *user); void fullscreen_action(GtkToggleAction *action, void *user); void reset_transformations_action(GtkAction *action, void *user); @@ -128,6 +160,7 @@ int scroll_event(GtkWidget *widget, GdkEventScroll *event, void *user); extern struct file *files; extern size_t nfiles; extern size_t current_file; +void destroy_file(struct file *file); void free_files(void); size_t files_from_array(struct file *selected_files, size_t nselected_files); size_t files_from_list(struct file **files_out, GSList *selected_files); @@ -140,26 +173,42 @@ void add_image_filters(GtkFileChooser *chooser); void free_image_filters(void); /* image.c */ -extern GdkPixbuf *transformed_image; -GdkPixbuf *get_image(void); -GdkPixbuf *get_original_image(void); +extern GdkPixbuf *transformed_raster; +void forget_transformed_raster(void); +int get_image(struct image *image); +int get_original_image(struct image *image); /* opening.c */ int load_image(struct file *file, int errors_to_stderr); GtkFileChooser *construct_chooser(void); /* printing.c */ -void open_print(GdkPixbuf *image); +void open_print(struct image *image); + +/* render.c */ +GdkPixbuf *rasterise(RsvgHandle *vector, int width, int height); +void paint_image(cairo_t *cr, struct image *image); + +/* preferences.c */ +extern cairo_filter_t scaling_filter; +extern GdkColor chequer_colours[2]; +extern uint16_t chequer_alphas[2]; +extern int chequered_background; +extern int chequer_size; +void open_preferences(void); /* properties.c */ +GtkWidget *create_property_table(struct file *file); void open_properties(struct file *file); /* transform.c */ extern enum transformation trans; -GdkPixbuf *apply_transformations(GdkPixbuf *image); +int apply_transformations(struct image *image, struct image *image_out); enum transformation reset_transformations_without_redraw(void); int add_transformation_without_redraw(enum transformation augment); +int swaps_dimensions(enum transformation t); void transform_point(enum transformation t, double *x, double *y, double *w, double *h); +int get_affine_matrix(cairo_matrix_t *matrix_out, enum transformation t, int width, int height); /* ui.c */ extern GtkWidget *window; @@ -170,6 +219,8 @@ void set_up_ui(void); void set_actions_sensitive(void); void clear_toggles(enum toggle_group group, GtkToggleAction *except); void set_fullscreen_mode(int active); +void set_property_pane_visible(int visible); +void update_property_pane(void); int activate_fullscreen(void *user); int deactivate_fullscreen(void *user); void set_statusbar_message(const char *message); @@ -183,8 +234,8 @@ double get_zoom(Zoom z); Zoom make_zoom(double s); Zoom zoom_in(Zoom z); Zoom zoom_out(Zoom z); -Zoom calculate_fit(GdkPixbuf *image, int xfit, int yfit, double cap); -Zoom calculate_zoom_mode(GdkPixbuf *image, enum zoom_mode mode); +Zoom calculate_fit(struct image *image, int xfit, int yfit, double cap); +Zoom calculate_zoom_mode(struct image *image, enum zoom_mode mode); /* gbild.c */ int expose_image(GtkWidget *widget, GdkEventExpose *event, void *user); @@ -4,5 +4,5 @@ MANPREFIX = $(PREFIX)/share/man CC = c99 CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE -CFLAGS = $$(pkg-config --cflags gtk+-2.0 gexiv2) -LDFLAGS = $$(pkg-config --libs gtk+-2.0 gexiv2) -lsimple -lm +CFLAGS = $$(pkg-config --cflags gtk+-2.0 gexiv2 librsvg-2.0) +LDFLAGS = $$(pkg-config --libs gtk+-2.0 gexiv2 librsvg-2.0) -lsimple -lm @@ -19,16 +19,27 @@ static int button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) { (void) user; - if (event->button == 2u || (event->state & GDK_CONTROL_MASK)) { - if (event->button == 1u) { - gtk_drag_source_unset(widget); - drag_source_disabled = 1; - } + switch (event->button) { + case 1u: + if (~event->state & GDK_CONTROL_MASK) + return 0; + gtk_drag_source_unset(widget); + drag_source_disabled = 1; + /* fall through */ + case 2u: dragging = 1; drag_x = (int)event->x_root; drag_y = (int)event->y_root; + return 1; + case 8u: + previous_action(NULL, NULL); + return 1; + case 9u: + next_action(NULL, NULL); + return 1; + default: + return 0; } - return dragging; } static int @@ -62,36 +73,40 @@ pointer_moved(GtkWidget *widget, GdkEventMotion *event, void *user) static void drag_begin(GtkWidget *widget, GdkDragContext *context, void *user) { - GdkPixbuf *image, *icon; + struct image image; + GdkPixbuf *icon; intmax_t height, width; + int w, h; (void) widget; (void) user; - image = get_original_image(); - if (!image) + if (!get_original_image(&image)) return; - width = gdk_pixbuf_get_width(image); - height = gdk_pixbuf_get_height(image); - if (width <= DRAG_ICON_SIZE && height <= DRAG_ICON_SIZE) { - gtk_drag_set_icon_pixbuf(context, image, 0, 0); - return; + width = (intmax_t)image.width; + height = (intmax_t)image.height; + if (width > DRAG_ICON_SIZE || height > DRAG_ICON_SIZE) { + if (width >= height) { + height = height * DRAG_ICON_SIZE / width; + height = MAX(1, height); + width = DRAG_ICON_SIZE; + } else { + width = width * DRAG_ICON_SIZE / height; + width = MAX(1, width); + height = DRAG_ICON_SIZE; + } } - if (width >= height) { - height = height * DRAG_ICON_SIZE / width; - height = MAX(1, height); - width = DRAG_ICON_SIZE; - } else { - width = width * DRAG_ICON_SIZE / height; - width = MAX(1, width); - height = DRAG_ICON_SIZE; + w = (int)width; + h = (int)height; + + if (image.raster && width == image.width && height == image.height) { + gtk_drag_set_icon_pixbuf(context, image.raster, 0, 0); + } else if ((image.vector && (icon = rasterise(image.vector, w, h))) || + (image.raster && (icon = gdk_pixbuf_scale_simple(image.raster, w, h, GDK_INTERP_BILINEAR)))) { + gtk_drag_set_icon_pixbuf(context, icon, 0, 0); + g_object_unref(icon); } - icon = gdk_pixbuf_scale_simple(image, (int)width, (int)height, GDK_INTERP_BILINEAR); - if (!icon) - return; - gtk_drag_set_icon_pixbuf(context, icon, 0, 0); - g_object_unref(icon); } static void @@ -4,7 +4,12 @@ static char url_list_name[] = "text/uri-list"; static GtkTargetEntry url_list = {url_list_name, 0u, 0u}; -static size_t +static unsigned int active_drop_timer; +static GdkDragContext *active_drop_context; +static GtkWidget *active_drop_widget; +static GdkDragAction active_drop_action; + +size_t get_dropped_files(struct file **files_out, char **uris) { GError *error = NULL; @@ -55,6 +60,18 @@ drop_files(GtkWidget *widget, GdkDragContext *context, int x, int y, (void) info; (void) user; + if (active_drop_timer) { + g_source_remove(active_drop_timer); + active_drop_timer = 0; + } + + if (gtk_drag_get_source_widget(context) == collection_view) { + if (widget == display_area) + navigate_collection_drag(); + else + accepted = 0; + goto out; + } if (gtk_drag_get_source_widget(context) == display_area) { accepted = 0; goto out; @@ -85,6 +102,31 @@ out: } static int +poll_modifiers(void *user) +{ + GdkModifierType modifiers; + GdkDragAction action = 0; + + (void) user; + + if (!GTK_WIDGET_REALIZED(active_drop_widget)) + return 1; + + gdk_window_get_pointer(active_drop_widget->window, NULL, NULL, &modifiers); + + action = (modifiers & GDK_CONTROL_MASK) ? GDK_ACTION_COPY : GDK_ACTION_MOVE; + if (!(gdk_drag_context_get_actions(active_drop_context) & action)) + action = 0; + + if (action != active_drop_action) { + active_drop_action = action; + gdk_drag_status(active_drop_context, action, GDK_CURRENT_TIME); + } + + return 1; +} + +static int drop_motion(GtkWidget *widget, GdkDragContext *context, int x, int y, unsigned int time, void *user) { GdkWindow *event_window = gtk_widget_get_window(widget); @@ -107,14 +149,35 @@ drop_motion(GtkWidget *widget, GdkDragContext *context, int x, int y, unsigned i out: gdk_drag_status(context, action, time); + + if (!active_drop_timer) { + active_drop_context = context; + active_drop_widget = widget; + active_drop_action = action; + active_drop_timer = g_timeout_add(50, &poll_modifiers, NULL); + } return 1; } +static void +drop_leave(GtkWidget *widget, GdkDragContext *context, unsigned int time, void *user) +{ + (void) widget; + (void) context; + (void) time; + (void) user; + if (active_drop_timer) { + g_source_remove(active_drop_timer); + active_drop_timer = 0; + } +} + void set_up_dropping(void) { gtk_drag_dest_set(window, GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP, &url_list, 1u, GDK_ACTION_COPY | GDK_ACTION_MOVE); g_signal_connect(window, "drag-motion", G_CALLBACK(&drop_motion), NULL); + g_signal_connect(window, "drag-leave", G_CALLBACK(&drop_leave), NULL); g_signal_connect(window, "drag-data-received", G_CALLBACK(&drop_files), NULL); } @@ -26,16 +26,14 @@ transform(enum transformation augment) static void set_zoom_fit(GtkToggleAction *action, enum zoom_mode mode) { - GdkPixbuf *image; - + struct image image; if (gtk_toggle_action_get_active(action)) { zoom_mode = mode; - if ((image = get_image())) - set_zoom_and_recentre(calculate_zoom_mode(image, zoom_mode)); + if (get_image(&image)) + set_zoom_and_recentre(calculate_zoom_mode(&image, zoom_mode)); } else if (zoom_mode == mode) { zoom_mode = ZOOM_CUSTOM; /* deselect zoom */ } - clear_toggles(GROUP_ZOOM, action); } @@ -70,9 +68,11 @@ reload_action(GtkAction *action, void *user) void print_action(GtkAction *action, void *user) { + struct image image; (void) action; (void) user; - open_print(get_image()); + if (get_image(&image)) + open_print(&image); } void @@ -84,6 +84,28 @@ properties_action(GtkAction *action, void *user) } void +preferences_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + open_preferences(); +} + +void +property_pane_action(GtkToggleAction *action, void *user) +{ + (void) user; + set_property_pane_visible(gtk_toggle_action_get_active(action)); +} + +void +collection_pane_action(GtkToggleAction *action, void *user) +{ + (void) user; + set_collection_pane_visible(gtk_toggle_action_get_active(action)); +} + +void close_action(GtkAction *action, void *user) { (void) action; @@ -278,6 +300,9 @@ key_pressed(GtkWidget *widget, GdkEventKey *event, void *user) if (event->state & GDK_CONTROL_MASK) return 0; + if (gtk_window_get_focus(GTK_WINDOW(window)) != display_area) + return 0; + amount = (event->state & GDK_MOD1_MASK) ? 1 : SCROLL_AMOUNT; hamount = (double)amount; vamount = (double)amount; @@ -5,17 +5,31 @@ struct file *files = NULL; size_t nfiles = 0u; size_t current_file = 0u; +static void +null_image_data(struct file *file) +{ + file->image.raster = NULL; + file->image.vector = NULL; + file->exif_tags = NULL; + file->exif = NULL; +} + +void +destroy_file(struct file *file) +{ + free(file->path); + g_clear_object(&file->image.raster); + g_clear_object(&file->image.vector); + if (file->exif_tags) + g_strfreev(file->exif_tags); + g_clear_object(&file->exif); +} + void free_files(void) { - while (nfiles) { - --nfiles; - free(files[nfiles].path); - g_clear_object(&files[nfiles].image); - if (files[nfiles].exif_tags) - g_strfreev(files[nfiles].exif_tags); - g_clear_object(&files[nfiles].exif); - } + while (nfiles) + destroy_file(&files[--nfiles]); free(files); files = NULL; current_file = 0u; @@ -27,9 +41,7 @@ files_from_array(struct file *selected_files, size_t nselected_files) size_t i, n; for (i = 0u, n = 0u; i < nselected_files; i++) { - selected_files[i].image = NULL; - selected_files[i].exif_tags = NULL; - selected_files[i].exif = NULL; + null_image_data(&selected_files[i]); if (load_image(&selected_files[i], 0)) { if (i != n) selected_files[n] = selected_files[i]; @@ -57,9 +69,7 @@ files_from_list(struct file **files_out, GSList *selected_files) for (file = selected_files, i = 0u; file; file = file->next) { new_files[i].path = estrdup(file->data); - new_files[i].image = NULL; - new_files[i].exif_tags = NULL; - new_files[i].exif = NULL; + null_image_data(&new_files[i]); g_free(file->data); if (load_image(&new_files[i], 0)) @@ -94,9 +104,7 @@ set_files_from_argv(int argc, char **argv) files = ecalloc(nfiles, sizeof(*files)); for (i = 0u; i < nfiles; i++) { files[i].path = estrdup(argv[i]); - files[i].image = NULL; - files[i].exif_tags = NULL; - files[i].exif = NULL; + null_image_data(&files[i]); if (!load_image(&files[i], 1)) { free(files[i].path); ok = 0; @@ -110,9 +118,7 @@ set_files_from_argv(int argc, char **argv) struct file * get_file(void) { - if (current_file >= nfiles) - return NULL; - return &files[current_file]; + return current_file < nfiles ? &files[current_file] : NULL; } int @@ -5,8 +5,13 @@ USAGE("[-f] [file] ..."); static int suspend_expose = 0; +static cairo_pattern_t *chequer_pattern; +static GdkColor cached_chequer_colours[2]; +static uint16_t cached_chequer_alphas[2]; +static int cached_chequer_size; + static size_t -format_statusbar_message(char *buffer, size_t size, struct file *file) +format_statusbar_message(char *buffer, size_t size, struct file *file, struct image *image) { char fsize_decimals[3]; const char units[] = "BKMGTPEZYRQ"; @@ -35,7 +40,7 @@ done: "Image %zu of %zu | %ju%s %c | %i × %i pixels | %g%% scale", current_file + 1u, nfiles, fsize, fsize_decimals, units[unit], - gdk_pixbuf_get_width(file->image), gdk_pixbuf_get_height(file->image), + image->width, image->height, 100.0 * get_zoom(zoom)); if (r < 0) @@ -49,21 +54,104 @@ update_status(void) struct file *file = get_file(); char *message = NULL; size_t len; - if (file) { - len = format_statusbar_message(NULL, 0u, file); + struct image image; + if (file && get_original_image(&image)) { + len = format_statusbar_message(NULL, 0u, file, &image); message = malloc(++len); - format_statusbar_message(message, len, file); + format_statusbar_message(message, len, file, &image); } set_statusbar_message(message); free(message); } +static cairo_pattern_t * +get_chequer_pattern(void) +{ + cairo_surface_t *surface; + cairo_t *cr; + int i, row, column; + double r[2], g[2], b[2], a[2], s; + + if (!chequer_pattern || cached_chequer_size != chequer_size) + goto changed; + for (i = 0; i < 2; i++) { + if (cached_chequer_colours[i].red != chequer_colours[i].red || + cached_chequer_colours[i].green != chequer_colours[i].green || + cached_chequer_colours[i].blue != chequer_colours[i].blue || + cached_chequer_alphas[i] != chequer_alphas[i]) + goto changed; + } + return chequer_pattern; + +changed: + for (i = 0; i < 2; i++) { + cached_chequer_colours[i] = chequer_colours[i]; + cached_chequer_alphas[i] = chequer_alphas[i]; + } + cached_chequer_size = chequer_size; + + for (i = 0; i < 2; i++) { + r[i] = (1.0 / 65535.0) * chequer_colours[i].red; + g[i] = (1.0 / 65535.0) * chequer_colours[i].green; + b[i] = (1.0 / 65535.0) * chequer_colours[i].blue; + a[i] = (1.0 / 65535.0) * chequer_alphas[i]; + } + + s = (double)chequer_size; + + surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 2 * chequer_size, 2 * chequer_size); + cr = cairo_create(surface); + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); + + for (row = 0; row < 2; row++) { + for (column = 0; column < 2; column++) { + i = (row ^ column) & 1; + cairo_set_source_rgba(cr, r[i], g[i], b[i], a[i]); + cairo_rectangle(cr, (double)column * s, (double)row * s, s, s); + cairo_fill(cr); + } + } + cairo_destroy(cr); + + if (chequer_pattern) + cairo_pattern_destroy(chequer_pattern); + chequer_pattern = cairo_pattern_create_for_surface(surface); + cairo_pattern_set_extend(chequer_pattern, CAIRO_EXTEND_REPEAT); + cairo_pattern_set_filter(chequer_pattern, CAIRO_FILTER_NEAREST); + + cairo_surface_destroy(surface); + return chequer_pattern; +} + +static void +draw_transparency_background(cairo_t *cr, struct image *image, double x0, double y0, double width, double height) +{ + cairo_matrix_t matrix; + + if (!chequered_background || gtk_toggle_action_get_active(fullscreen)) + return; + if (!image->vector && !gdk_pixbuf_get_has_alpha(image->raster)) + return; + + cairo_matrix_init_identity(&matrix); + cairo_pattern_set_matrix(get_chequer_pattern(), &matrix); + + cairo_save(cr); + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); + cairo_rectangle(cr, x0, y0, width, height); + cairo_clip(cr); + cairo_set_source(cr, chequer_pattern); + cairo_paint(cr); + cairo_restore(cr); +} + int expose_image(GtkWidget *widget, GdkEventExpose *event, void *user) { - GdkPixbuf *image; + struct image image; cairo_t *cr; double height, width, x, y, s; + RsvgRectangle viewport; (void) user; @@ -76,15 +164,14 @@ expose_image(GtkWidget *widget, GdkEventExpose *event, void *user) cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); cairo_rectangle(cr, event->area.x, event->area.y, event->area.width, event->area.height); - image = get_image(); - if (!image) { + if (!get_image(&image)) { cairo_fill(cr); goto out; } s = get_zoom(zoom); - width = s * gdk_pixbuf_get_width(image); - height = s * gdk_pixbuf_get_height(image); + width = s * (double)image.width; + height = s * (double)image.height; if (width < widget->allocation.width) x = 0.5 * ((double)widget->allocation.width - width); else @@ -95,13 +182,24 @@ expose_image(GtkWidget *widget, GdkEventExpose *event, void *user) y = -logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll))->value; cairo_fill(cr); + draw_transparency_background(cr, &image, x, y, width, height); cairo_translate(cr, x, y); cairo_scale(cr, s, s); - gdk_cairo_set_source_pixbuf(cr, image, 0.0, 0.0); + if (image.vector) { + viewport = (RsvgRectangle){0.0, 0.0, image.width, image.height}; + cairo_save(cr); + if (image.has_transformer) + cairo_transform(cr, &image.transformer); + if (rsvg_handle_render_document(image.vector, cr, &viewport, NULL)) + goto out; + cairo_restore(cr); + } + gdk_cairo_set_source_pixbuf(cr, image.raster, 0.0, 0.0); + cairo_pattern_set_filter(cairo_get_source(cr), scaling_filter); + cairo_paint(cr); out: - cairo_paint(cr); cairo_destroy(cr); return 1; } @@ -109,39 +207,40 @@ out: static void show_image(int update_zoom) { - GdkPixbuf *new_image, *original_image, *display_image; + struct image original_image, new_image, *display_image; + int transformed; double s; - original_image = get_original_image(); - if (!original_image) { + if (!get_original_image(&original_image)) { logical_scroll_pane_set_content_size(LOGICAL_SCROLL_PANE(scroll), 1, 1); gtk_widget_queue_draw(display_area); return; } - new_image = apply_transformations(original_image); - display_image = new_image ? new_image : original_image; + transformed = apply_transformations(&original_image, &new_image); + display_image = transformed ? &new_image : &original_image; if (update_zoom && zoom_mode != ZOOM_CUSTOM && zoom_mode != ZOOM_NORMAL) zoom = calculate_zoom_mode(display_image, zoom_mode); s = get_zoom(zoom); logical_scroll_pane_set_content_size(LOGICAL_SCROLL_PANE(scroll), - (int)(s * gdk_pixbuf_get_width(display_image)), - (int)(s * gdk_pixbuf_get_height(display_image))); + (int)(s * display_image->width), + (int)(s * display_image->height)); - if (transformed_image) - g_object_unref(transformed_image); - transformed_image = new_image; + forget_transformed_raster(); + if (transformed) + transformed_raster = new_image.raster; } static void set_images(struct file *new_files, size_t nnew_files) { - g_clear_object(&transformed_image); + forget_transformed_raster(); free_files(); files = new_files; nfiles = nnew_files; + collection_files_changed(); navigate_redraw(); set_actions_sensitive(); } @@ -156,6 +255,7 @@ add_images(struct file *new_files, size_t nnew_files) navigate_redraw(); else update_status(); + collection_files_changed(); set_actions_sensitive(); } @@ -205,34 +305,29 @@ set_zoom_around_point(Zoom new_zoom, double x, double y) GtkAdjustment *hadjustment, *vadjustment; double old_s, new_s, old_x0, old_y0, new_x0, new_y0; double iw, ih, vw, vh, hx, hy, adj_x, adj_y, max; - int w, h; - GdkPixbuf *image; + struct image image; if (zoom_eq(new_zoom, zoom)) return; - image = get_image(); - if (!image) + if (!get_image(&image)) goto out; old_s = get_zoom(zoom); new_s = get_zoom(new_zoom); zoom = new_zoom; - w = gdk_pixbuf_get_width(image); - h = gdk_pixbuf_get_height(image); - hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll)); vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll)); vw = hadjustment->page_size; vh = vadjustment->page_size; - iw = old_s * (double)w; - ih = old_s * (double)h; + iw = old_s * (double)image.width; + ih = old_s * (double)image.height; old_x0 = vw > iw ? 0.5 * (vw - iw) : 0.0; old_y0 = vh > ih ? 0.5 * (vh - ih) : 0.0; - iw = new_s * (double)w; - ih = new_s * (double)h; + iw = new_s * (double)image.width; + ih = new_s * (double)image.height; new_x0 = vw > iw ? 0.5 * (vw - iw) : 0.0; new_y0 = vh > ih ? 0.5 * (vh - ih) : 0.0; @@ -330,29 +425,30 @@ reload_redraw(void) suspend_expose--; if (!suspend_expose) gtk_widget_queue_draw(display_area); + update_property_pane(); + collection_files_changed(); update_status(); } void transform_redraw(enum transformation augment) { - GdkPixbuf *image; + struct image image; GtkAdjustment *hadjustment, *vadjustment; double s, x, y, width, height, max; hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll)); vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll)); - image = get_image(); - if (!image) + if (!get_image(&image)) return; s = get_zoom(zoom); - width = gdk_pixbuf_get_width(image); - height = gdk_pixbuf_get_height(image); + width = (double)image.width; + height = (double)image.height; x = hadjustment->value + 0.5 * hadjustment->page_size; - x = s * width > hadjustment->page_size ? x / s : 0.5 * width; y = vadjustment->value + 0.5 * vadjustment->page_size; + x = s * width > hadjustment->page_size ? x / s : 0.5 * width; y = s * height > vadjustment->page_size ? y / s : 0.5 * height; transform_point(augment, &x, &y, &width, &height); @@ -386,16 +482,18 @@ navigate_redraw(void) suspend_expose--; if (!suspend_expose) gtk_widget_queue_draw(display_area); + update_property_pane(); + update_collection(); update_status(); } int recalculate_zoom_mode(void *user) { - GdkPixbuf *image = get_image(); + struct image image; (void) user; - if (image && zoom_mode != ZOOM_CUSTOM && zoom_mode != ZOOM_NORMAL) - set_zoom_and_recentre(calculate_zoom_mode(image, zoom_mode)); + if (get_image(&image) && zoom_mode != ZOOM_CUSTOM && zoom_mode != ZOOM_NORMAL) + set_zoom_and_recentre(calculate_zoom_mode(&image, zoom_mode)); return 0; } @@ -438,8 +536,9 @@ main(int argc, char *argv[]) gtk_main(); - if (transformed_image) - g_object_unref(transformed_image); + if (chequer_pattern) + cairo_pattern_destroy(chequer_pattern); + forget_transformed_raster(); free_ui(); out: free_files(); @@ -1,35 +1,65 @@ /* See LICENSE file for copyright and license details. */ #include "common.h" -GdkPixbuf *transformed_image = NULL; +GdkPixbuf *transformed_raster = NULL; -static void -forget_transformed_image(void) +void +forget_transformed_raster(void) { - if (transformed_image) { - g_object_unref(transformed_image); - transformed_image = NULL; - } + g_clear_object(&transformed_raster); } -GdkPixbuf * -get_image(void) +int +get_image(struct image *image) { struct file *file = get_file(); - if (!file || !file->image) { - forget_transformed_image(); - return NULL; + if (!file || !file->image.raster) + forget_transformed_raster(); + if (!file) + return 0; + image->raster = transformed_raster ? transformed_raster : file->image.raster; + image->vector = (trans & ~7u) ? NULL : file->image.vector; /* TODO implement support for colour manipulation */ + if (swaps_dimensions(trans)) { + image->width = file->image.height; + image->height = file->image.width; + image->has_x_dpi = file->image.has_y_dpi; + image->has_y_dpi = file->image.has_x_dpi; + image->x_dpi = file->image.y_dpi; + image->y_dpi = file->image.x_dpi; + } else { + image->width = file->image.width; + image->height = file->image.height; + image->has_x_dpi = file->image.has_x_dpi; + image->has_y_dpi = file->image.has_y_dpi; + image->x_dpi = file->image.x_dpi; + image->y_dpi = file->image.y_dpi; } - return transformed_image ? transformed_image : file->image; + image->has_transformer = get_affine_matrix(&image->transformer, trans, file->image.width, file->image.height); + + /* TODO there is a bug in cairo or rsvg that prevents rendering when the + * linear part of the transformation has a negative component */ + if (trans) + image->vector = NULL; + + return 1; } -GdkPixbuf * -get_original_image(void) +int +get_original_image(struct image *image) { struct file *file = get_file(); - if (!file || !file->image) { - forget_transformed_image(); - return NULL; - } - return file->image; + if (!file || !file->image.raster) + forget_transformed_raster(); + if (!file) + return 0; + image->raster = file->image.raster; + image->vector = file->image.vector; + image->width = file->image.width; + image->height = file->image.height; + image->has_x_dpi = file->image.has_x_dpi; + image->has_y_dpi = file->image.has_y_dpi; + image->has_transformer = 0; + image->x_dpi = file->image.x_dpi; + image->y_dpi = file->image.y_dpi; + return 1; } @@ -64,14 +64,33 @@ fail: return -1; } +static int +get_raster_dpi_option(GdkPixbuf *raster, const char *name, double *dpi) +{ + const char *text = gdk_pixbuf_get_option(raster, name); + char *end; + double value; + if (!text) + return 0; + value = strtod(text, &end); + while (isspace(*(unsigned char *)end)) + end++; + if (end == text || *end || !isfinite(value) || value <= 0.0) + return 0; + *dpi = value; + return 1; +} + int load_image(struct file *file, int errors_to_stderr) { const char *error_message; GError *error = NULL; GtkWidget *dialog; - GdkPixbuf *image; + GdkPixbuf *raster; GdkPixbufLoader *loader = NULL; + GdkPixbufFormat *format; + RsvgHandle *vector = NULL; struct stat st; unsigned char *buffer = NULL; size_t size; @@ -100,14 +119,31 @@ load_image(struct file *file, int errors_to_stderr) goto fail_gerror; if (!gdk_pixbuf_loader_close(loader, &error)) goto fail_gerror; - image = gdk_pixbuf_loader_get_pixbuf(loader); - if (!image) { + raster = gdk_pixbuf_loader_get_pixbuf(loader); + if (!raster) { error_message = "Unsupported image format"; goto fail; } - g_object_ref(image); - g_clear_object(&file->image); - file->image = image; + format = gdk_pixbuf_loader_get_format(loader); + if (format) { + char *name = gdk_pixbuf_format_get_name(format); + if (!strcmp(name, "svg")) + vector = rsvg_handle_new_from_file(file->path, &error); + g_free(name); + if (!vector) + g_clear_error(&error); + } + g_object_ref(raster); + g_clear_object(&file->image.raster); + g_clear_object(&file->image.vector); + file->image.raster = raster; + file->image.vector = vector; + vector = NULL; + + file->image.width = gdk_pixbuf_get_width(file->image.raster); + file->image.height = gdk_pixbuf_get_height(file->image.raster); + file->image.has_x_dpi = get_raster_dpi_option(file->image.raster, "x-dpi", &file->image.x_dpi); + file->image.has_y_dpi = get_raster_dpi_option(file->image.raster, "y-dpi", &file->image.y_dpi); if (file->exif_tags) { g_strfreev(file->exif_tags); @@ -120,6 +156,7 @@ load_image(struct file *file, int errors_to_stderr) out: free(buffer); + g_clear_object(&vector); g_clear_object(&loader); if (fd >= 0) close(fd); diff --git a/preferences.c b/preferences.c new file mode 100644 index 0000000..e4d73e2 --- /dev/null +++ b/preferences.c @@ -0,0 +1,223 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +#define LIST_SCALING_FILTERS(X, ...)\ + X(0, CAIRO_FILTER_NEAREST, "Nearest") __VA_ARGS__\ + X(1, CAIRO_FILTER_BILINEAR, "Bilinear") __VA_ARGS__\ + X(2, CAIRO_FILTER_GAUSSIAN, "Gaussian") + +cairo_filter_t scaling_filter = CAIRO_FILTER_BILINEAR; +GdkColor chequer_colours[2] = { + {0u, 0xD3D3u, 0xD3D3u, 0xD3D3u}, + {0u, 0xFFFFu, 0xFFFFu, 0xFFFFu} +}; +uint16_t chequer_alphas[2] = {0xFFFFu, 0xFFFFu}; +int chequered_background = 0; +int chequer_size = 16; + +static GtkWidget *preferences_window; +static GtkWidget *transparency_controls; + +static void +preferences_response(GtkDialog *dialog, int response, void *user) +{ + (void) response; + (void) user; + gtk_widget_destroy(GTK_WIDGET(dialog)); +} + +static void +preferences_destroyed(GtkWidget *widget, void *user) +{ + (void) widget; + (void) user; + preferences_window = NULL; +} + +static void +scaling_filter_changed(GtkComboBox *box, void *user) +{ + (void) user; + switch (gtk_combo_box_get_active(box)) { +#define X(I, V, T) case I: scaling_filter = V; break + LIST_SCALING_FILTERS(X, ;); +#undef X + default: + abort(); + } + gtk_widget_queue_draw(display_area); +} + +static void +chequered_background_changed(GtkToggleButton *button, void *user) +{ + (void) user; + chequered_background = gtk_toggle_button_get_active(button); + gtk_widget_set_sensitive(transparency_controls, chequered_background); + gtk_widget_queue_draw(display_area); +} + +static void +transparency_colour_changed(GtkColorButton *button, void *user) +{ + GdkColor *colour = user; + size_t index = (size_t)(colour - chequer_colours); + gtk_color_button_get_color(button, colour); + chequer_alphas[index] = gtk_color_button_get_alpha(button); + gtk_widget_queue_draw(display_area); +} + +static void +chequer_size_changed(GtkSpinButton *button, void *user) +{ + (void) user; + chequer_size = gtk_spin_button_get_value_as_int(button); + gtk_widget_queue_draw(display_area); +} + +static GtkWidget * +new_section(const char *title) +{ + GtkWidget *frame = gtk_frame_new(title); + GtkWidget *box = gtk_vbox_new(0, 6); + gtk_container_set_border_width(GTK_CONTAINER(box), 6u); + gtk_container_add(GTK_CONTAINER(frame), box); + return frame; +} + +static GtkWidget * +new_scaling_section(void) +{ + GtkWidget *section = new_section("Scaling"); + GtkWidget *box = gtk_bin_get_child(GTK_BIN(section)); + GtkWidget *row = gtk_hbox_new(0, 6); + GtkWidget *label = gtk_label_new_with_mnemonic("_Algorithm:"); + GtkWidget *filters = gtk_combo_box_new_text(); + +#define X(I, V, T) gtk_combo_box_append_text(GTK_COMBO_BOX(filters), T) + LIST_SCALING_FILTERS(X, ;); +#undef X + + switch (scaling_filter) { +#define X(I, V, T) case V: gtk_combo_box_set_active(GTK_COMBO_BOX(filters), I); break + LIST_SCALING_FILTERS(X, ;); +#undef X + case CAIRO_FILTER_FAST: + case CAIRO_FILTER_GOOD: + case CAIRO_FILTER_BEST: + default: + abort(); + } + + gtk_label_set_mnemonic_widget(GTK_LABEL(label), filters); + gtk_box_pack_start(GTK_BOX(row), label, 0, 0, 0u); + gtk_box_pack_start(GTK_BOX(row), filters, 0, 0, 0u); + gtk_box_pack_start(GTK_BOX(box), row, 0, 0, 0u); + g_signal_connect(filters, "changed", G_CALLBACK(&scaling_filter_changed), NULL); + return section; +} + +#ifdef TODO +static GtkWidget * +new_unimplemented_section(void) +{ + GtkWidget *section = new_section("Image handling"); + GtkWidget *box = gtk_bin_get_child(GTK_BIN(section)); + GtkWidget *orientation; + GtkWidget *physical_size; + + orientation = gtk_check_button_new_with_mnemonic("Apply embedded _orientation"); + physical_size = gtk_check_button_new_with_mnemonic("Normal size is _physical size"); + gtk_box_pack_start(GTK_BOX(box), orientation, 0, 0, 0u); + gtk_box_pack_start(GTK_BOX(box), physical_size, 0, 0, 0u); + return section; +} +#endif + +static GtkWidget * +new_transparency_section(void) +{ + GtkWidget *section = new_section("Transparent images"); + GtkWidget *box = gtk_bin_get_child(GTK_BIN(section)); + GtkWidget *enabled; + GtkWidget *first; + GtkWidget *second; + GtkWidget *size; + GtkWidget *label; + GtkWidget *units; + + enabled = gtk_check_button_new_with_mnemonic("Use che_quered background"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enabled), chequered_background); + gtk_box_pack_start(GTK_BOX(box), enabled, 0, 0, 0u); + transparency_controls = gtk_table_new(3u, 3u, 0); + gtk_table_set_row_spacings(GTK_TABLE(transparency_controls), 3u); + gtk_table_set_col_spacings(GTK_TABLE(transparency_controls), 6u); + gtk_box_pack_start(GTK_BOX(box), transparency_controls, 0, 0, 0u); + + label = gtk_label_new_with_mnemonic("_First colour:"); + gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5); + first = gtk_color_button_new_with_color(&chequer_colours[0]); + gtk_color_button_set_use_alpha(GTK_COLOR_BUTTON(first), 1); + gtk_color_button_set_alpha(GTK_COLOR_BUTTON(first), chequer_alphas[0]); + gtk_label_set_mnemonic_widget(GTK_LABEL(label), first); + gtk_table_attach(GTK_TABLE(transparency_controls), label, 0u, 1u, 0u, 1u, GTK_FILL, GTK_FILL, 0u, 0u); + gtk_table_attach(GTK_TABLE(transparency_controls), first, 1u, 2u, 0u, 1u, GTK_FILL, GTK_FILL, 0u, 0u); + + label = gtk_label_new_with_mnemonic("_Second colour:"); + gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5); + second = gtk_color_button_new_with_color(&chequer_colours[1]); + gtk_color_button_set_use_alpha(GTK_COLOR_BUTTON(second), 1); + gtk_color_button_set_alpha(GTK_COLOR_BUTTON(second), chequer_alphas[1]); + gtk_label_set_mnemonic_widget(GTK_LABEL(label), second); + gtk_table_attach(GTK_TABLE(transparency_controls), label, 0u, 1u, 1u, 2u, GTK_FILL, GTK_FILL, 0u, 0u); + gtk_table_attach(GTK_TABLE(transparency_controls), second, 1u, 2u, 1u, 2u, GTK_FILL, GTK_FILL, 0u, 0u); + + label = gtk_label_new_with_mnemonic("Chequer si_ze:"); + gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5); + size = gtk_spin_button_new_with_range(1.0, 256.0, 1.0); + units = gtk_label_new("pixels"); + gtk_misc_set_alignment(GTK_MISC(units), 0.0, 0.5); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(size), (double)chequer_size); + gtk_label_set_mnemonic_widget(GTK_LABEL(label), size); + gtk_table_attach(GTK_TABLE(transparency_controls), label, 0u, 1u, 2u, 3u, GTK_FILL, GTK_FILL, 0u, 0u); + gtk_table_attach(GTK_TABLE(transparency_controls), size, 1u, 2u, 2u, 3u, GTK_FILL, GTK_FILL, 0u, 0u); + gtk_table_attach(GTK_TABLE(transparency_controls), units, 2u, 3u, 2u, 3u, GTK_FILL, GTK_FILL, 0u, 0u); + + gtk_widget_set_sensitive(transparency_controls, chequered_background); + g_signal_connect(enabled, "toggled", G_CALLBACK(&chequered_background_changed), NULL); + g_signal_connect(first, "color-set", G_CALLBACK(&transparency_colour_changed), &chequer_colours[0]); + g_signal_connect(second, "color-set", G_CALLBACK(&transparency_colour_changed), &chequer_colours[1]); + g_signal_connect(size, "value-changed", G_CALLBACK(&chequer_size_changed), NULL); + return section; +} + +void +open_preferences(void) +{ + GtkWidget *alignment, *box; + + if (preferences_window) { + gtk_window_present(GTK_WINDOW(preferences_window)); + return; + } + + preferences_window = gtk_dialog_new_with_buttons("Preferences", GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL); + + alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); + gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), 6u, 6u, 6u, 6u); + gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(preferences_window))), alignment, 1, 1, 0u); + box = gtk_vbox_new(0, 6); + gtk_container_add(GTK_CONTAINER(alignment), box); + + gtk_box_pack_start(GTK_BOX(box), new_scaling_section(), 0, 0, 0u); +#ifdef TODO + gtk_box_pack_start(GTK_BOX(box), new_unimplemented_section(), 0, 0, 0u); +#endif + gtk_box_pack_start(GTK_BOX(box), new_transparency_section(), 0, 0, 0u); + + g_signal_connect(preferences_window, "response", G_CALLBACK(&preferences_response), NULL); + g_signal_connect(preferences_window, "destroy", G_CALLBACK(&preferences_destroyed), NULL); + + gtk_widget_show_all(preferences_window); +} @@ -39,7 +39,7 @@ enum field { #define NFIELDS (LIST_FIELDS(X_COUNT, +)) struct print_data { - GdkPixbuf *image; + struct image *image; double width; double height; double left; @@ -118,31 +118,12 @@ get_printable_size(GtkPageSetup *setup, double *width, double *height) } static int -get_dpi_option(GdkPixbuf *image, const char *name, double *dpi) +get_physical_size(struct image *image, double *width, double *height) { - const char *text = gdk_pixbuf_get_option(image, name); - char *end; - double value; - if (!text) - return 0; - value = strtod(text, &end); - while (isspace(*(unsigned char *)end)) - end++; - if (end == text || *end || !isfinite(value) || value <= 0.0) - return 0; - *dpi = value; - return 1; -} - -static int -get_print_size_from_resolution(GdkPixbuf *image, double *width, double *height) -{ - double x_dpi, y_dpi; - if (!get_dpi_option(image, "x-dpi", &x_dpi) || - !get_dpi_option(image, "y-dpi", &y_dpi)) + if (!image->has_x_dpi || !image->has_y_dpi) return 0; - *width = 72.0 * gdk_pixbuf_get_width(image) / x_dpi; - *height = 72.0 * gdk_pixbuf_get_height(image) / y_dpi; + *width = 72.0 * (double)image->width / image->x_dpi; + *height = 72.0 * (double)image->height / image->y_dpi; return 1; } @@ -284,14 +265,13 @@ positioning_preview_expose(GtkWidget *preview, GdkEventExpose *event, void *user double scale = get_preview_scale(preview, widget); double w = scale * widget->page_width; double h = scale * widget->page_height; - double page_x = preview->allocation.width - 8.0 - w; + double page_x; double page_y = 8.0; - int width, height; cairo_t *cr; - width = gdk_pixbuf_get_width(data->image); - height = gdk_pixbuf_get_height(data->image); - + w = MAX(round(w), 1.0); + h = MAX(round(h), 1.0); + page_x = round(preview->allocation.width - 8.0 - w); cr = gdk_cairo_create(preview->window); cairo_rectangle(cr, event->area.x, event->area.y, event->area.width, event->area.height); @@ -299,25 +279,30 @@ positioning_preview_expose(GtkWidget *preview, GdkEventExpose *event, void *user gdk_cairo_set_source_color(cr, &preview->style->bg[GTK_STATE_NORMAL]); cairo_paint(cr); + cairo_save(cr); + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); cairo_rectangle(cr, page_x, page_y, w, h); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_fill(cr); + cairo_restore(cr); cairo_save(cr); cairo_rectangle(cr, page_x, page_y, w, h); cairo_clip(cr); cairo_translate(cr, page_x + scale * data->left, page_y + scale * data->top); - cairo_scale(cr, scale * data->width / width, scale * data->height / height); - gdk_cairo_set_source_pixbuf(cr, data->image, 0.0, 0.0); - cairo_paint(cr); + cairo_scale(cr, scale * data->width / (double)data->image->width, scale * data->height / (double)data->image->height); + paint_image(cr, data->image); cairo_restore(cr); - cairo_rectangle(cr, page_x, page_y, w, h); + cairo_save(cr); + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); + cairo_rectangle(cr, page_x + 0.5, page_y + 0.5, w - 1.0, h - 1.0); cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_set_line_width(cr, 1.0); cairo_stroke(cr); + cairo_restore(cr); cairo_destroy(cr); return 0; } @@ -742,63 +727,34 @@ static void draw_page(GtkPrintOperation *operation, GtkPrintContext *context, int page, void *user) { struct print_data *data = user; - GdkPixbuf *image = data->image; - int width, height; cairo_t *cr; - (void) operation; (void) page; - - width = gdk_pixbuf_get_width(image); - height = gdk_pixbuf_get_height(image); - cr = gtk_print_context_get_cairo_context(context); cairo_translate(cr, data->left, data->top); - cairo_scale(cr, data->width / (double)width, data->height / (double)height); - gdk_cairo_set_source_pixbuf(cr, image, 0.0, 0.0); - cairo_paint(cr); + cairo_scale(cr, data->width / (double)data->image->width, data->height / (double)data->image->height); + paint_image(cr, data->image); } void -open_print(GdkPixbuf *image) +open_print(struct image *image) { struct print_data data; GtkPrintOperation *operation; GtkPageSetup *setup; - GdkPixbuf *metadata_image; GtkPageOrientation orientation = GTK_PAGE_ORIENTATION_PORTRAIT; - double page_width, page_height, s, x, y, temporary; + double page_width, page_height, s, x, y; int has_physical_size; - int width, height; if (!image) return; data.image = image; - g_object_ref(data.image); - width = gdk_pixbuf_get_width(data.image); - height = gdk_pixbuf_get_height(data.image); - metadata_image = get_original_image(); - if (!metadata_image) - metadata_image = data.image; - has_physical_size = get_print_size_from_resolution(metadata_image, &data.width, &data.height); + has_physical_size = get_physical_size(image, &data.width, &data.height); if (!has_physical_size) { - data.width = width; - data.height = height; - } else { - switch (trans & 7u) { - case TRANS_CLOCKWISE: - case TRANS_ANTICLOCKWISE: - case TRANS_TRANSPOSE: - case TRANS_ANTITRANSPOSE: - temporary = data.width; - data.width = data.height; - data.height = temporary; - break; - default: - break; - } + data.width = (double)image->width; + data.height = (double)image->height; } data.aspect = data.width / data.height; @@ -833,5 +789,4 @@ open_print(GdkPixbuf *image) gtk_print_operation_run(operation, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW(window), NULL); g_object_unref(operation); - g_object_unref(data.image); } diff --git a/properties.c b/properties.c index 0fe02cc..edf2536 100644 --- a/properties.c +++ b/properties.c @@ -1,23 +1,6 @@ /* See LICENSE file for copyright and license details. */ #include "common.h" -static int -get_dpi_option(GdkPixbuf *image, const char *name, double *dpi) -{ - const char *text = gdk_pixbuf_get_option(image, name); - char *end; - double value; - if (!text) - return 0; - value = strtod(text, &end); - while (isspace(*(unsigned char *)end)) - end++; - if (end == text || *end || !isfinite(value) || value <= 0.0) - return 0; - *dpi = value; - return 1; -} - static GtkWidget * new_metadata_label(const char *text) { @@ -29,12 +12,15 @@ new_metadata_label(const char *text) } static void -add_metadata_row(GtkTable *table, guint row, const char *name, const char *value) +add_metadata_row(GtkTable *table, guint row, PangoAttrList *name_attributes, const char *name, const char *value) { + GtkWidget *property = gtk_vbox_new(0, 0); GtkWidget *name_label = new_metadata_label(name); GtkWidget *value_label = new_metadata_label(value); - gtk_table_attach(table, name_label, 0, 1, row, row + 1, GTK_FILL, GTK_FILL, 0, 0); - gtk_table_attach(table, value_label, 1, 2, row, row + 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 12, 0); + gtk_label_set_attributes(GTK_LABEL(name_label), name_attributes); + gtk_box_pack_start(GTK_BOX(property), name_label, 0, 0, 0); + gtk_box_pack_start(GTK_BOX(property), value_label, 0, 0, 0); + gtk_table_attach(table, property, 0, 1, row, row + 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); } static int @@ -119,10 +105,10 @@ format_file_size(off_t size) } static char * -format_pixel_size(GdkPixbuf *image) +format_pixel_size(GdkPixbuf *raster) { - int w = gdk_pixbuf_get_width(image); - int h = gdk_pixbuf_get_height(image); + int w = gdk_pixbuf_get_width(raster); + int h = gdk_pixbuf_get_height(raster); char *text = emalloc(2u * 3u * sizeof(w) + sizeof(" × pixels")); if (sprintf(text, "%i × %i pixels", w, h) < 0) eprintf("sprintf:"); @@ -171,33 +157,26 @@ format_physical_size(int pixels, double resolution) return text; } -static char * -format_physical_width(GdkPixbuf *image, double resolution) -{ - return format_physical_size(gdk_pixbuf_get_width(image), resolution); -} - -static char * -format_physical_height(GdkPixbuf *image, double resolution) -{ - return format_physical_size(gdk_pixbuf_get_height(image), resolution); -} - -static GtkWidget * +GtkWidget * create_property_table(struct file *file) { GtkWidget *table; - unsigned i, row = 0u, rows = 3u; - const char *orientation; - char *text, orientation_buf[3u * sizeof(long int) + 1u]; - double x_dpi, y_dpi; - int has_x_dpi, has_y_dpi; + PangoContext *context; + PangoFontMetrics *metrics; + PangoAttrList *title_attributes; + unsigned i, row = 0u, rows = 2u; + const char *orientation = NULL; + char *text, *tag, orientation_buf[3u * sizeof(long int) + 1u]; + int spacing; GList *keys, *key; GHashTable *options; GError *error; long int li; - options = gdk_pixbuf_get_options(file->image); + title_attributes = pango_attr_list_new(); + pango_attr_list_insert(title_attributes, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + + options = gdk_pixbuf_get_options(file->image.raster); keys = g_list_sort(g_hash_table_get_keys(options), (GCompareFunc)&strcmp); for (key = keys; key; key = key->next) @@ -205,69 +184,69 @@ create_property_table(struct file *file) rows += file->exif_tags ? (unsigned)g_strv_length(file->exif_tags) : 0u; - orientation = gdk_pixbuf_get_option(file->image, "orientation"); - if (!orientation && file->exif) { - error = NULL; - li = gexiv2_metadata_try_get_tag_long(file->exif, "Exif.Image.Orientation", &error); - if (error) - g_clear_error(&error); - else if (sprintf(orientation_buf, "%li", li) < 0) - eprintf("sprintf:"); - else - orientation = orientation_buf; + if (file->image.raster && !file->image.vector) { + rows += 1u; + + orientation = gdk_pixbuf_get_option(file->image.raster, "orientation"); + if (!orientation && file->exif) { + error = NULL; + li = gexiv2_metadata_try_get_tag_long(file->exif, "Exif.Image.Orientation", &error); + if (error) + g_clear_error(&error); + else if (sprintf(orientation_buf, "%li", li) < 0) + eprintf("sprintf:"); + else + orientation = orientation_buf; + } + rows += orientation ? 1u : 0u; } - rows += orientation ? 1u : 0u; - has_x_dpi = get_dpi_option(file->image, "x-dpi", &x_dpi); - has_y_dpi = get_dpi_option(file->image, "y-dpi", &y_dpi); - rows += 2u * (unsigned)(has_x_dpi + has_y_dpi); - table = gtk_table_new(rows, 2, 0); - gtk_table_set_row_spacings(GTK_TABLE(table), 6); - gtk_table_set_col_spacings(GTK_TABLE(table), 12); + table = gtk_table_new(rows, 1, 0); + context = gtk_widget_get_pango_context(table); + metrics = pango_context_get_metrics(context, NULL, pango_context_get_language(context)); + spacing = PANGO_PIXELS(pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)); + spacing /= 2; + pango_font_metrics_unref(metrics); + gtk_table_set_row_spacings(GTK_TABLE(table), (guint)spacing); - add_metadata_row(GTK_TABLE(table), row++, "File", file->path); - add_metadata_row(GTK_TABLE(table), row++, "File size", text = format_file_size(file->size)); - free(text); - add_metadata_row(GTK_TABLE(table), row++, "Pixel dimensions", text = format_pixel_size(file->image)); - free(text); - if (orientation) { - add_metadata_row(GTK_TABLE(table), row++, "Orientation", text = format_orientation(orientation)); - free(text); - } - if (has_x_dpi) { - add_metadata_row(GTK_TABLE(table), row++, "Horizontal resolution", text = format_resolution(x_dpi)); - free(text); - } - if (has_y_dpi) { - add_metadata_row(GTK_TABLE(table), row++, "Vertical resolution", text = format_resolution(y_dpi)); - free(text); - } - if (has_x_dpi) { - add_metadata_row(GTK_TABLE(table), row++, "Physical width", text = format_physical_width(file->image, x_dpi)); - free(text); - } - if (has_y_dpi) { - add_metadata_row(GTK_TABLE(table), row++, "Physical height", text = format_physical_height(file->image, x_dpi)); - free(text); - } +#define ROW(NAME, VALUE)\ + (text = NULL, add_metadata_row(GTK_TABLE(table), row++, title_attributes, NAME, VALUE), free(text)) + + ROW("File", file->path); + ROW("File size", text = format_file_size(file->size)); + if (file->image.raster && !file->image.vector) + ROW("Pixel dimensions", text = format_pixel_size(file->image.raster)); + if (orientation) + ROW("Orientation", text = format_orientation(orientation)); + if (file->image.has_x_dpi) + ROW("Horizontal resolution", text = format_resolution(file->image.x_dpi)); + if (file->image.has_y_dpi) + ROW("Vertical resolution", text = format_resolution(file->image.y_dpi)); + if (file->image.has_x_dpi) + ROW("Physical width", text = format_physical_size(file->image.width, file->image.x_dpi)); + if (file->image.has_y_dpi) + ROW("Physical height", text = format_physical_size(file->image.height, file->image.y_dpi)); for (key = keys; key; key = key->next) if (is_unrecognised_metadata(key->data)) - add_metadata_row(GTK_TABLE(table), row++, key->data, g_hash_table_lookup(options, key->data)); + ROW(key->data, g_hash_table_lookup(options, key->data)); g_list_free(keys); if (file->exif_tags) { for (i = 0u; file->exif_tags[i]; i++) { error = NULL; - text = gexiv2_metadata_try_get_tag_interpreted_string(file->exif, file->exif_tags[i], &error); + tag = gexiv2_metadata_try_get_tag_interpreted_string(file->exif, file->exif_tags[i], &error); g_clear_error(&error); - - add_metadata_row(GTK_TABLE(table), row++, file->exif_tags[i], text ? text : ""); - g_free(text); + if (tag) + ROW(file->exif_tags[i], tag); + g_free(tag); } } +#undef ROW + + pango_attr_list_unref(title_attributes); return table; } @@ -277,7 +256,7 @@ open_properties(struct file *file) GtkWidget *dialog, *scrolled; char *title; - if (!file || !file->path || !file->image) + if (!file || !file->path) return; dialog = gtk_dialog_new_with_buttons(title = format_title(file), diff --git a/render.c b/render.c new file mode 100644 index 0000000..fdc2f0c --- /dev/null +++ b/render.c @@ -0,0 +1,118 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +#include <string.h> + +static uint8_t +unpremultiply(double colour, double alpha) +{ + if (colour <= 0.04045) + colour = colour / 12.92; + else + colour = pow((colour + 0.055) / 1.055, 2.4); + + colour /= alpha; + + if (colour <= 0.0031308) + colour = 12.92 * colour; + else + colour = fma(1.055, pow(colour, 1.0 / 2.4), -0.055); + + colour = fma(255.0, colour, 0.5); + if (colour <= 0.0) + return 0u; + if (colour >= 255.0) + return 255u; + return (uint8_t)colour; +} + +GdkPixbuf * +rasterise(RsvgHandle *vector, int width, int height) +{ + RsvgRectangle viewport = {0.0, 0.0, width, height}; + cairo_surface_t *surface = NULL; + cairo_t *cr = NULL; + GdkPixbuf *image; + int x, y, sstride, dstride; + const uint8_t *s, *sy; + uint8_t *d, *dy; + double a, r, g, b; + uint32_t pixel; + + surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) + goto fail; + + cr = cairo_create(surface); + if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) + goto fail; + + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.0); + cairo_paint(cr); + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + if (!rsvg_handle_render_document(vector, cr, &viewport, NULL)) + goto fail; + cairo_destroy(cr); + cr = NULL; + + image = gdk_pixbuf_new(GDK_COLORSPACE_RGB, 1, 8, width, height); + if (!image) + goto fail; + + /* TODO accelerate */ + cairo_surface_flush(surface); + sy = s = cairo_image_surface_get_data(surface); + dy = d = gdk_pixbuf_get_pixels(image); + sstride = cairo_image_surface_get_stride(surface); + dstride = gdk_pixbuf_get_rowstride(image); + for (y = 0; y < height; y++, sy = s = &s[sstride], dy = d = &d[dstride]) { + for (x = 0; x < width; x++, sy = &sy[4u], dy = &dy[4u]) { + memcpy(&pixel, sy, 4u); + if ((uint8_t)(pixel >> 24)) { + a = (double)(uint8_t)(pixel >> 24) / 255.0; + r = (double)(uint8_t)(pixel >> 16) / 255.0; + g = (double)(uint8_t)(pixel >> 8) / 255.0; + b = (double)(uint8_t)(pixel >> 0) / 255.0; + dy[0] = unpremultiply(r, a); + dy[1] = unpremultiply(g, a); + dy[2] = unpremultiply(b, a); + dy[3] = (uint8_t)(pixel >> 24); + } else { + dy[0] = dy[1] = dy[2] = dy[3] = 0u; + } + } + } + + cairo_surface_destroy(surface); + return image; + +fail: + if (cr) + cairo_destroy(cr); + if (surface) + cairo_surface_destroy(surface); + return NULL; +} + +void +paint_image(cairo_t *cr, struct image *image) +{ + RsvgRectangle viewport = {0.0, 0.0, image->width, image->height}; + int rendered; + + if (image->vector) { + cairo_save(cr); + if (image->has_transformer) + cairo_transform(cr, &image->transformer); + rendered = rsvg_handle_render_document(image->vector, cr, &viewport, NULL); + cairo_restore(cr); + if (rendered) + return; + } + + if (image->raster) { + gdk_cairo_set_source_pixbuf(cr, image->raster, 0.0, 0.0); + cairo_paint(cr); + } +} diff --git a/transform.c b/transform.c index b9461d7..4bc4e6e 100644 --- a/transform.c +++ b/transform.c @@ -4,21 +4,21 @@ enum transformation trans = TRANS_NONE; static GdkPixbuf * -make_negative(GdkPixbuf *image) +make_negative_raster(GdkPixbuf *raster) { - GdkPixbuf *new_image; + GdkPixbuf *new_raster; uint8_t *pixels; int width, height, rowstride, channels, x, y; - new_image = gdk_pixbuf_copy(image); - if (!new_image) + new_raster = gdk_pixbuf_copy(raster); + if (!new_raster) exit(1); - pixels = gdk_pixbuf_get_pixels(new_image); - width = gdk_pixbuf_get_width(new_image); - height = gdk_pixbuf_get_height(new_image); - rowstride = gdk_pixbuf_get_rowstride(new_image); - channels = gdk_pixbuf_get_n_channels(new_image); + pixels = gdk_pixbuf_get_pixels(new_raster); + width = gdk_pixbuf_get_width(new_raster); + height = gdk_pixbuf_get_height(new_raster); + rowstride = gdk_pixbuf_get_rowstride(new_raster); + channels = gdk_pixbuf_get_n_channels(new_raster); for (y = 0; y < height; y++) { for (x = 0; x < width * channels; x += channels) { @@ -48,62 +48,91 @@ make_negative(GdkPixbuf *image) pixels = &pixels[rowstride]; } - return new_image; + return new_raster; } static GdkPixbuf * -apply_rotation_and_reflection(GdkPixbuf *image) +apply_rotation_and_reflection(GdkPixbuf *raster) { - GdkPixbuf *new_image, *newer_image; + GdkPixbuf *new_raster, *newer_raster; switch (trans & 7u) { case TRANS_NONE: return NULL; case TRANS_CLOCKWISE: - new_image = gdk_pixbuf_rotate_simple(image, GDK_PIXBUF_ROTATE_CLOCKWISE); + new_raster = gdk_pixbuf_rotate_simple(raster, GDK_PIXBUF_ROTATE_CLOCKWISE); break; case TRANS_180_DEGREES: - new_image = gdk_pixbuf_rotate_simple(image, GDK_PIXBUF_ROTATE_UPSIDEDOWN); + new_raster = gdk_pixbuf_rotate_simple(raster, GDK_PIXBUF_ROTATE_UPSIDEDOWN); break; case TRANS_ANTICLOCKWISE: - new_image = gdk_pixbuf_rotate_simple(image, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE); + new_raster = gdk_pixbuf_rotate_simple(raster, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE); break; case TRANS_FLIP: case TRANS_TRANSPOSE: - new_image = gdk_pixbuf_flip(image, 0); + new_raster = gdk_pixbuf_flip(raster, 0); break; case TRANS_FLOP: case TRANS_ANTITRANSPOSE: - new_image = gdk_pixbuf_flip(image, 1); + new_raster = gdk_pixbuf_flip(raster, 1); break; } - if (!new_image) + if (!new_raster) exit(1); switch (trans & 7u) { case TRANS_TRANSPOSE: case TRANS_ANTITRANSPOSE: - newer_image = gdk_pixbuf_rotate_simple(new_image, GDK_PIXBUF_ROTATE_CLOCKWISE); - if (!newer_image) + newer_raster = gdk_pixbuf_rotate_simple(new_raster, GDK_PIXBUF_ROTATE_CLOCKWISE); + if (!newer_raster) exit(1); - g_object_unref(new_image); - return newer_image; + g_object_unref(new_raster); + return newer_raster; default: - return new_image; + return new_raster; } } -GdkPixbuf * -apply_transformations(GdkPixbuf *image) +int +apply_transformations(struct image *image, struct image *image_out) { - GdkPixbuf *negative_image, *new_image; - negative_image = (trans & TRANS_NEGATIVE) ? make_negative(image) : NULL; - new_image = apply_rotation_and_reflection(negative_image ? negative_image : image); - if (!new_image) - new_image = negative_image ? negative_image : image; - else if (negative_image) - g_object_unref(negative_image); - return new_image == image ? NULL : new_image; + GdkPixbuf *negative_raster, *new_raster; + + if (trans == TRANS_NONE) + return 0; + + if (image->raster) { + negative_raster = (trans & TRANS_NEGATIVE) ? make_negative_raster(image->raster) : NULL; + new_raster = apply_rotation_and_reflection(negative_raster ? negative_raster : image->raster); + if (!new_raster) + new_raster = negative_raster; + else if (negative_raster) + g_object_unref(negative_raster); + image_out->raster = new_raster; + if (!image_out->raster) + return 0; + } + + image_out->vector = image->vector; + + if (swaps_dimensions(trans)) { + image_out->width = image->height; + image_out->height = image->width; + image_out->has_x_dpi = image->has_y_dpi; + image_out->has_y_dpi = image->has_x_dpi; + image_out->x_dpi = image->y_dpi; + image_out->y_dpi = image->x_dpi; + } else { + image_out->width = image->width; + image_out->height = image->height; + image_out->has_x_dpi = image->has_x_dpi; + image_out->has_y_dpi = image->has_y_dpi; + image_out->x_dpi = image->x_dpi; + image_out->y_dpi = image->y_dpi; + } + image_out->has_transformer = get_affine_matrix(&image_out->transformer, trans, image->width, image->height); + + return 1; } static enum transformation @@ -131,14 +160,31 @@ add_transformation_without_redraw(enum transformation augment) return trans != old; } +int +swaps_dimensions(enum transformation t) +{ + switch (t & 7u) { + case TRANS_NONE: + case TRANS_180_DEGREES: + case TRANS_FLIP: + case TRANS_FLOP: + return 0; + case TRANS_CLOCKWISE: + case TRANS_ANTICLOCKWISE: + case TRANS_TRANSPOSE: + case TRANS_ANTITRANSPOSE: + return 1; + } + abort(); +} + void transform_point(enum transformation t, double *x, double *y, double *w, double *h) { double x1 = *x, y1 = *y, x2 = *w, y2 = *h; - switch (t & 7u) { case TRANS_NONE: - return; + break; case TRANS_CLOCKWISE: *x = y2 - y1; *y = x1; @@ -146,7 +192,7 @@ transform_point(enum transformation t, double *x, double *y, double *w, double * case TRANS_180_DEGREES: *x = x2 - x1; *y = y2 - y1; - return; + break; case TRANS_ANTICLOCKWISE: *x = y1; *y = x2 - x1; @@ -157,16 +203,35 @@ transform_point(enum transformation t, double *x, double *y, double *w, double * break; case TRANS_FLIP: *y = y2 - y1; - return; + break; case TRANS_ANTITRANSPOSE: *x = y2 - y1; *y = x2 - x1; break; case TRANS_FLOP: *x = x2 - x1; - return; + break; } + if (swaps_dimensions(t)) { + *w = y2; + *h = x2; + } +} - *w = y2; - *h = x2; +int +get_affine_matrix(cairo_matrix_t *matrix_out, enum transformation t, int width, int height) +{ + double w = (double)width; + double h = (double)height; + switch (t & 7u) { + case TRANS_NONE: cairo_matrix_init(matrix_out, +1.0, 0.0, 0.0, +1.0, 0.0, 0.0); return 0; + case TRANS_CLOCKWISE: cairo_matrix_init(matrix_out, 0.0, +1.0, -1.0, 0.0, h, 0.0); break; + case TRANS_180_DEGREES: cairo_matrix_init(matrix_out, -1.0, 0.0, 0.0, -1.0, w, h); break; + case TRANS_ANTICLOCKWISE: cairo_matrix_init(matrix_out, 0.0, -1.0, +1.0, 0.0, 0.0, w); break; + case TRANS_TRANSPOSE: cairo_matrix_init(matrix_out, 0.0, +1.0, +1.0, 0.0, 0.0, 0.0); break; + case TRANS_FLOP: cairo_matrix_init(matrix_out, -1.0, 0.0, 0.0, +1.0, w, 0.0); break; + case TRANS_ANTITRANSPOSE: cairo_matrix_init(matrix_out, 0.0, -1.0, -1.0, 0.0, h, w); break; + case TRANS_FLIP: cairo_matrix_init(matrix_out, +1.0, 0.0, 0.0, -1.0, 0.0, h); break; + } + return 1; } @@ -9,10 +9,13 @@ ACTION(0, 1u, "", "Print", GTK_STOCK_PRINT, "<control>P", "Ctrl+P", print_action, D)\ SEPARATOR(0, D)\ ACTION(0, 1u, "Proper", "ties", GTK_STOCK_PROPERTIES, "<alt>Return", "Alt+Return", properties_action, D)\ + ACTION(0, 0u, "Pr", "eferences", GTK_STOCK_PREFERENCES, NULL, "", preferences_action, D)\ SEPARATOR(0, D)\ ACTION(0, 0u, "", "Close", GTK_STOCK_CLOSE, "<control>W", "Ctrl+W", close_action, D)\ TOPLEVEL(0, "", "View", D)\ TOGGLE(1, 1u, "F", "ullscreen", GTK_STOCK_FULLSCREEN, "F11", "F11", fullscreen_action, GROUP_NONE, 0, D)\ + TOGGLE(0, 1u, "", "Property pane", GTK_STOCK_PROPERTIES, "F9", "F9", property_pane_action, GROUP_NONE, 0, D)\ + TOGGLE(0, 0u, "Image ", "collection", GTK_STOCK_INDEX, "<control>F9", "Ctrl+F9", collection_pane_action, GROUP_NONE, 0, D)\ SEPARATOR(1, D)\ ACTION(0, 1u, "Re", "set transformations", "document-revert", "<alt>R", "Alt+R", reset_transformations_action, D)\ ACTION(1, 1u, "Rotate anti", "clockwise", "image-rotate-left", "<shift>R", "Shift+R", rotate_anticlockwise_action, D)\ @@ -27,7 +30,7 @@ SEPARATOR(1, D)\ ACTION(1, 1u, "", "Zoom in", "zoom-in", "plus", "+", zoom_in_action, D)\ ACTION(1, 1u, "Zoom ", "out", "zoom-out", "minus", "-", zoom_out_action, D)\ - TOGGLE(1, 1u, "", "Normal size", "zoom-original", "0", "0", normal_size_action, GROUP_ZOOM, 0, D)\ + TOGGLE(1, 1u, "", "Normal size", "zoom-original", "1", "1", normal_size_action, GROUP_ZOOM, 0, D)\ TOGGLE(1, 1u, "Fit to ", "width", "zoom-fit-width", "<control>H", "Ctrl+H", width_fit_action, GROUP_ZOOM, 0, D)\ TOGGLE(1, 1u, "Fit to ", "height", "zoom-fit-height", "<control>V", "Ctrl+V", height_fit_action, GROUP_ZOOM, 0, D)\ TOGGLE(1, 1u, "", "Fit to window", "zoom-fit-page", "<control><shift>F", "Ctrl+Shift+F", window_fit_action, GROUP_ZOOM, 0, D)\ @@ -47,7 +50,9 @@ GtkToggleAction *fullscreen; static GtkWidget *statusbar; static GtkWidget *menubar; static GtkWidget *toolbar; +static GtkWidget *property_pane; static GtkActionGroup *actions; +static GdkColor fullscreen_background = {0u, 0u, 0u, 0u}; #define X_SKIP(...) #define X_TOPLEVEL(T, A, B, ...) {A B, NULL, A "_" B, NULL, NULL, NULL} __VA_ARGS__ @@ -158,11 +163,20 @@ window_state_changed(GtkWidget *widget, GdkEventWindowState *event, void *user) return 0; } +static int +display_area_button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) +{ + (void) event; + (void) user; + gtk_widget_grab_focus(widget); + return 0; +} + void set_up_ui(void) { GtkAccelGroup *accelerators; - GtkWidget *box; + GtkWidget *box, *paned, *content; window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "gbild"); @@ -188,20 +202,31 @@ set_up_ui(void) gtk_box_pack_start(GTK_BOX(box), menubar, 0, 0, 0u); gtk_box_pack_start(GTK_BOX(box), toolbar, 0, 0, 0u); display_area = gtk_drawing_area_new(); + gtk_widget_set_can_focus(display_area, 1); gtk_widget_set_app_paintable(display_area, 1); gtk_widget_set_double_buffered(display_area, 0); scroll = logical_scroll_pane_new(); logical_scroll_pane_set_child(LOGICAL_SCROLL_PANE(scroll), display_area); gtk_widget_add_events(scroll, GDK_SCROLL_MASK); - g_signal_connect(scroll, "scroll-event", G_CALLBACK(&scroll_event), NULL); - gtk_box_pack_start(GTK_BOX(box), scroll, 1, 1, 0u); + paned = gtk_hpaned_new(); + property_pane = gtk_vbox_new(0, 0); + gtk_widget_set_no_show_all(property_pane, 1); + gtk_paned_pack1(GTK_PANED(paned), property_pane, 0, 1); + gtk_paned_pack2(GTK_PANED(paned), scroll, 1, 0); + gtk_paned_set_position(GTK_PANED(paned), INITIAL_PROPERTY_PANE_HEIGHT); + content = gtk_vbox_new(0, 0); + gtk_box_pack_start(GTK_BOX(content), paned, 1, 1, 0u); + gtk_box_pack_end(GTK_BOX(content), create_collection_pane(), 0, 0, 0u); + gtk_box_pack_start(GTK_BOX(box), content, 1, 1, 0u); statusbar = gtk_statusbar_new(); gtk_box_pack_end(GTK_BOX(box), statusbar, 0, 0, 0u); gtk_widget_add_events(display_area, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK); + g_signal_connect(display_area, "button-press-event", G_CALLBACK(&display_area_button_pressed), NULL); g_signal_connect(display_area, "expose-event", G_CALLBACK(&expose_image), NULL); + g_signal_connect(scroll, "scroll-event", G_CALLBACK(&scroll_event), NULL); } void @@ -262,6 +287,8 @@ clear_toggles(enum toggle_group group, GtkToggleAction *except) void set_fullscreen_mode(int active) { + gtk_widget_modify_bg(display_area, GTK_STATE_NORMAL, active ? &fullscreen_background : NULL); + gtk_widget_queue_draw(display_area); gtk_widget_set_visible(menubar, !active); gtk_widget_set_visible(toolbar, !active); gtk_widget_set_visible(statusbar, !active); @@ -274,6 +301,42 @@ set_fullscreen_mode(int active) gtk_window_unfullscreen(GTK_WINDOW(window)); } +void +set_property_pane_visible(int visible) +{ + struct file *file; + GtkWidget *properties; + GList *children, *child; + + if (!visible) { + gtk_widget_hide(property_pane); + g_idle_add(&recalculate_zoom_mode, NULL); + return; + } + + children = gtk_container_get_children(GTK_CONTAINER(property_pane)); + for (child = children; child; child = child->next) + gtk_widget_destroy(child->data); + g_list_free(children); + file = get_file(); + if (file && file->path) { + properties = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(properties), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_box_pack_start(GTK_BOX(property_pane), properties, 1, 1, 0); + gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(properties), create_property_table(file)); + gtk_widget_show_all(properties); + } + gtk_widget_show(property_pane); + g_idle_add(&recalculate_zoom_mode, NULL); +} + +void +update_property_pane(void) +{ + if (gtk_widget_get_visible(property_pane)) + set_property_pane_visible(1); +} + int activate_fullscreen(void *user) { @@ -35,22 +35,21 @@ zoom_out(Zoom z) } Zoom -calculate_fit(GdkPixbuf *image, int xfit, int yfit, double cap) +calculate_fit(struct image *image, int xfit, int yfit, double cap) { GtkAdjustment *adjustment; double s = 1.0, y; - int w, h; if (xfit) { adjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll)); - w = gdk_pixbuf_get_width(image); - s = adjustment->page_size / (double)w; + s = (double)image->width; + s = adjustment->page_size / s; } if (yfit) { adjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll)); - h = gdk_pixbuf_get_height(image); - y = adjustment->page_size / (double)h; + y = (double)image->height; + y = adjustment->page_size / y; s = xfit ? MIN(s, y) : y; } @@ -61,21 +60,15 @@ calculate_fit(GdkPixbuf *image, int xfit, int yfit, double cap) } Zoom -calculate_zoom_mode(GdkPixbuf *image, enum zoom_mode mode) +calculate_zoom_mode(struct image *image, enum zoom_mode mode) { switch (mode) { - case ZOOM_CUSTOM: - break; - case ZOOM_NORMAL: - return calculate_fit(image, 0, 0, 1.0); - case ZOOM_FIT_WIDTH: - return calculate_fit(image, 1, 0, HUGE_VAL); - case ZOOM_FIT_HEIGHT: - return calculate_fit(image, 0, 1, HUGE_VAL); - case ZOOM_FIT_WINDOW: - return calculate_fit(image, 1, 1, HUGE_VAL); - case ZOOM_BEST_FIT: - return calculate_fit(image, 1, 1, 1.0); + case ZOOM_CUSTOM: break; + case ZOOM_NORMAL: return calculate_fit(image, 0, 0, 1.0); + case ZOOM_FIT_WIDTH: return calculate_fit(image, 1, 0, HUGE_VAL); + case ZOOM_FIT_HEIGHT: return calculate_fit(image, 0, 1, HUGE_VAL); + case ZOOM_FIT_WINDOW: return calculate_fit(image, 1, 1, HUGE_VAL); + case ZOOM_BEST_FIT: return calculate_fit(image, 1, 1, 1.0); } return zoom; } |
