diff options
| author | Mattias Andrée <m@maandree.se> | 2026-10-14 18:54:48 +0200 |
|---|---|---|
| committer | Mattias Andrée <m@maandree.se> | 2026-10-14 18:54:48 +0200 |
| commit | 6bbfc093885043abdfa18826511ee053e52d08fa (patch) | |
| tree | 2d76b446617658412930ba4147e0d2db837d000a | |
| parent | Fourth commit (diff) | |
| download | gbild-master.tar.gz gbild-master.tar.bz2 gbild-master.tar.xz | |
Signed-off-by: Mattias Andrée <m@maandree.se>
| -rw-r--r-- | README | 68 | ||||
| -rw-r--r-- | TODO | 27 | ||||
| -rw-r--r-- | collection.c | 102 | ||||
| -rw-r--r-- | common.h | 64 | ||||
| -rw-r--r-- | dragging.c | 79 | ||||
| -rw-r--r-- | events.c | 40 | ||||
| -rw-r--r-- | gbild.1 | 94 | ||||
| -rw-r--r-- | gbild.c | 45 | ||||
| -rw-r--r-- | image.c | 62 | ||||
| -rw-r--r-- | opening.c | 19 | ||||
| -rw-r--r-- | preferences.c | 141 | ||||
| -rw-r--r-- | properties.c | 141 | ||||
| -rw-r--r-- | transform.c | 75 | ||||
| -rw-r--r-- | ui.c | 26 | ||||
| -rw-r--r-- | zoom.c | 101 |
15 files changed, 894 insertions, 190 deletions
@@ -1,14 +1,74 @@ NAME - gbild - GTK+ 2 image viewer + gbild - GTK+ 2 image viewer SYNOPSIS - gbild [-f] [file] ... + gbild [-s algorithm] [-S size] [-C colour[:colour]] [-z zoom] [-fpctn] [+fpctnC] [file] ... DESCRIPTION - gbild is a GTK+ 2 image viewer. + gbild is a GTK+ 2 image viewer. OPTIONS - -f Start in full-screen mode. + -f Start in full-screen mode. + + +f Do not start in full-screen mode. + + -p Show the property pane. + + +p Hide the property pane. + + -c Show the image collection. + + +c Hide the image collection. + + -t Apply an image's embedded orientation. + + +t Do not apply an image's embedded orientation. + + -n Use the image resolution and display resolution to make + normal size a physical size. + + +n Use one source pixel per display pixel for normal size. + + -s algorithm + Set the scaling algorithm. Supported values are nearest, + bilinear, and gaussian. + + -S size + Set the chequer square size in pixels. A size of 0 + disables the chequered background. + + -C colour1[:colour2] + Enable the chequered background and set its colours. + If the second colour is omitted, the first colour is used + for both squares. + + Colours use hexadecimal RGB, RRGGBB, RRRRGGGGBBBB, RGBA, + RRGGBBAA, or RRRRGGGGBBBBAAAA notation, with an optional + #, 0x, or 0X prefix. RGB-only forms may have a leading + alpha percentage; for example, 25%ABCDEF sets alpha to + 25 percent. + + +C Disable the chequered background. + + -z zoom + Set the initial zoom. zoom shall either be the zoom + expressed in perfect (e.g. 100%) or ratio (e.g. 1:1), + or one of the following modes: + + width + Fit to width + + height + Fit to height + + window + Fit to window + + best + Best fit + + stretch + Stretch to window OPERANDS file @@ -1,34 +1,19 @@ -Use hand cursor when dragging image to move it - -Properties shall display SVG data - -Render icon of image when also when dragging from the image collection +Create thumbnails for the image collection as they become visible; + cache the thumbnail in the `struct file` + (also store the thumbnail size used and whether embedded transformation was applied) Save preferences to file +Do not all of populate_collection() when new files are added without removing old files + Automatically go to next picture Animation support -"Properties" shall display Exif thumbnail if embedded - Colour space support Add support for bicubic interpolation Add support for Lanczos resampling -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 +Need another library to display more SVG metadata diff --git a/collection.c b/collection.c index c39f80b..a8a1df1 100644 --- a/collection.c +++ b/collection.c @@ -8,6 +8,7 @@ static GtkCellRenderer *collection_renderer; static GtkListStore *collection_model; static int collection_height = -1; static size_t popup_file; +static int drag_disabled; static char url_list_name[] = "text/uri-list"; static GtkTargetEntry url_list = {url_list_name, 0u, 0u}; @@ -36,7 +37,7 @@ collection_thumbnail_size(void) static GdkPixbuf * create_thumbnail(struct file *file, int maximum_size) { - GdkPixbuf *thumbnail = NULL; + GdkPixbuf *thumbnail = NULL, *scaled; int width = file->image.width; int height = file->image.height; double w = (double)width; @@ -47,14 +48,16 @@ create_thumbnail(struct file *file, int maximum_size) height = (int)(s * h); width = MAX(1, width); height = MAX(1, height); - if (file->image.vector) { + if (file->image.vector) thumbnail = rasterise(file->image.vector, width, height); - if (thumbnail) - return thumbnail; + if (!thumbnail && file->image.raster) { + scaled = gdk_pixbuf_scale_simple(file->image.raster, width, height, GDK_INTERP_BILINEAR); + if (!apply_embedded_orientation || !file->image.embedded_transformation) + return scaled; + thumbnail = transform_raster(scaled, file->image.embedded_transformation); + g_object_unref(scaled); } - if (file->image.raster) - return gdk_pixbuf_scale_simple(file->image.raster, width, height, GDK_INTERP_BILINEAR); - return NULL; + return thumbnail; } static void @@ -188,7 +191,7 @@ reorder_file(size_t source, size_t target) current_file += 1u; } files[target] = moved; - collection_files_changed(); + collection_files_changed(); /* TODO full populate_collection() is not needed, use gtk_list_store_move_before/after */ navigate_redraw(); } @@ -217,6 +220,16 @@ drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data } static void +drag_begin(GtkWidget *widget, GdkDragContext *context, void *user) +{ + size_t index; + (void) widget; + (void) user; + if (get_selected_index(&index) && index < nfiles) + set_image_drag_icon(context, &files[index].image); +} + +static void drag_data_received(GtkWidget *widget, GdkDragContext *context, int x, int y, GtkSelectionData *data, unsigned int info, unsigned int time, void *user) { @@ -283,8 +296,12 @@ collection_button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) { GtkWidget *menu, *item; size_t index; - (void) widget; (void) user; + if (event->button == 1u && !collection_index_at((int)event->x, (int)event->y, &index)) { + gtk_drag_source_unset(widget); + drag_disabled = 1; + return 0; + } if (event->button != 3u || !collection_index_at((int)event->x, (int)event->y, &index)) return 0; popup_file = index; @@ -298,6 +315,18 @@ collection_button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) return 1; } +static int +collection_button_released(GtkWidget *widget, GdkEventButton *event, void *user) +{ + (void) event; + (void) user; + if (drag_disabled) { + gtk_drag_source_set(widget, GDK_BUTTON1_MASK, &url_list, 1u, GDK_ACTION_COPY | GDK_ACTION_MOVE); + drag_disabled = 0; + } + return 0; +} + GtkWidget * create_collection_pane(void) { @@ -317,7 +346,7 @@ create_collection_pane(void) 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_widget_add_events(collection_view, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_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); @@ -325,9 +354,14 @@ create_collection_pane(void) 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, "button-release-event", G_CALLBACK(&collection_button_released), NULL); + g_signal_connect(collection_view, "drag-begin", G_CALLBACK(&drag_begin), 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); + gtk_adjustment_configure(gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(collection_pane)), + 0.0, 0.0, 1.0, 1.0, 1.0, 1.0); + return collection_pane; } @@ -340,6 +374,10 @@ set_collection_pane_visible(int visible) populate_collection(); select_current_file(); gtk_widget_grab_focus(collection_view); + if (!nfiles) { + gtk_adjustment_configure(gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(collection_pane)), + 0.0, 0.0, 1.0, 1.0, 1.0, 1.0); + } } else { gtk_widget_hide(collection_pane); gtk_widget_grab_focus(display_area); @@ -362,3 +400,47 @@ update_collection(void) if (collection_pane && gtk_widget_get_visible(collection_pane)) select_current_file(); } + +void +collection_redraw_embedded_orientations(void) +{ + GtkTreeIter iter; + GtkTreeModel *model; + int thumbnail_size; + GdkPixbuf *thumbnail; + size_t i; + + if (!nfiles || !collection_pane || !gtk_widget_get_visible(collection_pane)) + return; + + thumbnail_size = collection_thumbnail_size(); + + model = GTK_TREE_MODEL(collection_model); + if (!gtk_tree_model_get_iter_first(model, &iter)) + goto repopulate; + + for (i = 0u;;) { + if (!files[i].image.embedded_transformation) + goto next; + + thumbnail = create_thumbnail(&files[i], thumbnail_size); + if (!thumbnail) + goto next; + + gtk_list_store_set(collection_model, &iter, COLLECTION_THUMBNAIL, + thumbnail, COLLECTION_INDEX, (unsigned long)i, -1); + g_object_unref(thumbnail); + + next: + if (++i == nfiles) + break; + if (!gtk_tree_model_iter_next(model, &iter)) + goto repopulate; + } + + return; + +repopulate: + populate_collection(); + select_current_file(); +} @@ -11,6 +11,9 @@ #ifndef PREVIEW_HEIGHT # define PREVIEW_HEIGHT 256 #endif +#ifndef DEFAULT_CHEQUER_SIZE +# define DEFAULT_CHEQUER_SIZE 16 +#endif #ifndef INITIAL_COLLECTION_PANE_HEIGHT # define INITIAL_COLLECTION_PANE_HEIGHT 160 #endif @@ -36,6 +39,7 @@ #if defined(__GNUC__) # pragma GCC diagnostic ignored "-Wunsuffixed-float-constants" +# pragma GCC diagnostic ignored "-Waggregate-return" # pragma GCC diagnostic ignored "-Wsuggest-attribute=const" # pragma GCC diagnostic ignored "-Wsuggest-attribute=pure" #endif @@ -54,6 +58,24 @@ #define COMMA , +#define LIST_ORIENTATIONS(X, ...)\ + X(1, TRANS_NONE, "Normal") __VA_ARGS__\ + X(2, TRANS_FLOP, "Flip horizontally") __VA_ARGS__\ + X(3, TRANS_180_DEGREES, "Rotate 180°") __VA_ARGS__\ + X(4, TRANS_FLIP, "Flip vertically") __VA_ARGS__\ + X(5, TRANS_TRANSPOSE, "Transpose") __VA_ARGS__\ + X(6, TRANS_CLOCKWISE, "Rotate 90° clockwise") __VA_ARGS__\ + X(7, TRANS_ANTITRANSPOSE, "Antitranspose") __VA_ARGS__\ + X(8, TRANS_ANTICLOCKWISE, "Rotate 90° counterclockwise") + +#define LIST_ZOOM_MODES(X, ...)\ + X(ZOOM_FIT_WIDTH, width_fit_action, "width", "Fit to width", 1, 0, 0, HUGE_VAL) __VA_ARGS__\ + X(ZOOM_FIT_HEIGHT, height_fit_action, "height", "Fit to height", 0, 1, 0, HUGE_VAL) __VA_ARGS__\ + X(ZOOM_FIT_WINDOW, window_fit_action, "window", "Fit to window", 1, 1, 0, HUGE_VAL) __VA_ARGS__\ + X(ZOOM_BEST_FIT, best_fit_action, "best", "Best fit", 1, 1, 0, 1.0) __VA_ARGS__\ + X(ZOOM_STRETCH, stretch_action, "stretch", "Stretch to window", 1, 1, 1, HUGE_VAL) + + enum toggle_group { GROUP_NONE, GROUP_ZOOM @@ -61,14 +83,13 @@ enum toggle_group { enum zoom_mode { ZOOM_CUSTOM, - ZOOM_NORMAL, - ZOOM_FIT_WIDTH, - ZOOM_FIT_HEIGHT, - ZOOM_FIT_WINDOW, - ZOOM_BEST_FIT +#define X(E, ...) E + LIST_ZOOM_MODES(X, COMMA) +#undef X }; enum transformation { + /* NB! different order than in LIST_ORIENTATIONS */ TRANS_NONE, /* 1 */ TRANS_CLOCKWISE, /* r */ TRANS_180_DEGREES, /* r² */ @@ -91,6 +112,7 @@ struct image { int height; int has_x_dpi; int has_y_dpi; + enum transformation embedded_transformation; int has_transformer; cairo_matrix_t transformer; double x_dpi; @@ -105,7 +127,10 @@ struct file { GExiv2Metadata *exif; }; -typedef double Zoom; /* TODO */ +typedef struct { + double s; + int i; +} Zoom; /* collection.c */ @@ -115,8 +140,10 @@ void set_collection_pane_visible(int visible); void navigate_collection_drag(void); void collection_files_changed(void); void update_collection(void); +void collection_redraw_embedded_orientations(void); /* dragging.c */ +void set_image_drag_icon(GdkDragContext *context, struct image *image); void set_up_dragging(void); /* dropping.c */ @@ -145,10 +172,9 @@ void invert_action(GtkToggleAction *action, void *user); void zoom_in_action(GtkAction *action, void *user); void zoom_out_action(GtkAction *action, void *user); void normal_size_action(GtkToggleAction *action, void *user); -void width_fit_action(GtkToggleAction *action, void *user); -void height_fit_action(GtkToggleAction *action, void *user); -void window_fit_action(GtkToggleAction *action, void *user); -void best_fit_action(GtkToggleAction *action, void *user); +#define X(E, A, ...) void A(GtkToggleAction *action, void *user) +LIST_ZOOM_MODES(X, ;); +#undef X void next_action(GtkAction *action, void *user); void previous_action(GtkAction *action, void *user); void first_action(GtkAction *action, void *user); @@ -175,8 +201,8 @@ void free_image_filters(void); /* image.c */ extern GdkPixbuf *transformed_raster; void forget_transformed_raster(void); -int get_image(struct image *image); -int get_original_image(struct image *image); +int get_image(struct image *image_out); +int get_original_image(struct image *image_out); /* opening.c */ int load_image(struct file *file, int errors_to_stderr); @@ -195,7 +221,12 @@ extern GdkColor chequer_colours[2]; extern uint16_t chequer_alphas[2]; extern int chequered_background; extern int chequer_size; +extern int apply_embedded_orientation; +extern int normal_size_is_physical_size; /* TODO implement */ void open_preferences(void); +int parse_and_set_scaling_filter(const char *s); +int parse_and_set_chequer_size(const char *s); +int parse_and_set_chequer_colours(const char *s); /* properties.c */ GtkWidget *create_property_table(struct file *file); @@ -203,12 +234,16 @@ void open_properties(struct file *file); /* transform.c */ extern enum transformation trans; +enum transformation multiply_transformations(enum transformation augment, enum transformation augend); int apply_transformations(struct image *image, struct image *image_out); +GdkPixbuf *transform_raster(GdkPixbuf *raster, enum transformation transformation); +enum transformation inverse_transformation(enum transformation t); 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); +enum transformation orientation_to_transformation(int orientation); /* ui.c */ extern GtkWidget *window; @@ -219,7 +254,9 @@ 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_visible_panes(int property, int collection); void set_property_pane_visible(int visible); +void set_zoom_mode_toggle(void); void update_property_pane(void); int activate_fullscreen(void *user); int deactivate_fullscreen(void *user); @@ -229,13 +266,14 @@ void free_ui(void); /* zoom.c */ extern Zoom zoom; extern enum zoom_mode zoom_mode; +extern const Zoom normal_zoom; int zoom_eq(Zoom a, Zoom b); double get_zoom(Zoom z); Zoom make_zoom(double s); Zoom zoom_in(Zoom z); Zoom zoom_out(Zoom z); -Zoom calculate_fit(struct image *image, int xfit, int yfit, double cap); Zoom calculate_zoom_mode(struct image *image, enum zoom_mode mode); +int parse_and_set_zoom_mode(const char *s); /* gbild.c */ int expose_image(GtkWidget *widget, GdkEventExpose *event, void *user); @@ -9,6 +9,27 @@ static int drag_source_disabled = 0; static int drag_x, drag_y; static void +set_pan_cursor(GtkWidget *widget, int active) +{ + GdkWindow *gdk_window = gtk_widget_get_window(widget); + GdkCursor *cursor; + + if (!gdk_window) + return; + if (!active) { + gdk_window_set_cursor(gdk_window, NULL); + return; + } + cursor = gdk_cursor_new_from_name(gtk_widget_get_display(widget), "grabbing"); + if (!cursor) + cursor = gdk_cursor_new(GDK_HAND1); + if (!cursor) + return; + gdk_window_set_cursor(gdk_window, cursor); + gdk_cursor_unref(cursor); +} + +static void set_file_drag_source(GtkWidget *widget) { gtk_drag_source_set(widget, GDK_BUTTON1_MASK, &url_list, 1u, @@ -18,16 +39,24 @@ set_file_drag_source(GtkWidget *widget) static int button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) { + struct file *file; (void) user; switch (event->button) { case 1u: - if (~event->state & GDK_CONTROL_MASK) + if (~event->state & GDK_CONTROL_MASK) { + file = get_file(); + if (file && file->path) + return 0; + gtk_drag_source_unset(widget); + drag_source_disabled = 1; return 0; + } gtk_drag_source_unset(widget); drag_source_disabled = 1; /* fall through */ case 2u: dragging = 1; + set_pan_cursor(widget, 1); drag_x = (int)event->x_root; drag_y = (int)event->y_root; return 1; @@ -47,6 +76,8 @@ button_released(GtkWidget *widget, GdkEventButton *event, void *user) { (void) event; (void) user; + if (dragging) + set_pan_cursor(widget, 0); dragging = 0; if (drag_source_disabled) { set_file_drag_source(widget); @@ -70,22 +101,20 @@ pointer_moved(GtkWidget *widget, GdkEventMotion *event, void *user) return dragging; } -static void -drag_begin(GtkWidget *widget, GdkDragContext *context, void *user) +void +set_image_drag_icon(GdkDragContext *context, struct image *image) { - struct image image; - GdkPixbuf *icon; + GdkPixbuf *icon, *icon2; + enum transformation orientation; intmax_t height, width; int w, h; - (void) widget; - (void) user; - - if (!get_original_image(&image)) + if (!image) return; - width = (intmax_t)image.width; - height = (intmax_t)image.height; + orientation = image->embedded_transformation; + 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; @@ -100,16 +129,36 @@ drag_begin(GtkWidget *widget, GdkDragContext *context, void *user) 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)))) { + if (image->raster && width == image->width && height == image->height) { + if (orientation && (icon = transform_raster(image->raster, orientation))) + goto set_icon; + gtk_drag_set_icon_pixbuf(context, image->raster, 0, 0); + } else if (image->vector && (icon = rasterise(image->vector, w, h))) { + goto set_icon; + } else if (image->raster && (icon = gdk_pixbuf_scale_simple(image->raster, w, h, GDK_INTERP_BILINEAR))) { + if (orientation && (icon2 = transform_raster(icon, orientation))) { + g_object_unref(icon); + icon = icon2; + } + set_icon: gtk_drag_set_icon_pixbuf(context, icon, 0, 0); g_object_unref(icon); } } static void +drag_begin(GtkWidget *widget, GdkDragContext *context, void *user) +{ + struct image image; + + (void) widget; + (void) user; + + if (get_original_image(&image)) + set_image_drag_icon(context, &image); +} + +static void drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data, unsigned int info, unsigned int time, void *user) { @@ -218,37 +218,21 @@ zoom_out_action(GtkAction *action, void *user) void normal_size_action(GtkToggleAction *action, void *user) { + (void) action; (void) user; - set_zoom_fit(action, ZOOM_NORMAL); -} - -void -width_fit_action(GtkToggleAction *action, void *user) -{ - (void) user; - set_zoom_fit(action, ZOOM_FIT_WIDTH); -} - -void -height_fit_action(GtkToggleAction *action, void *user) -{ - (void) user; - set_zoom_fit(action, ZOOM_FIT_HEIGHT); -} - -void -window_fit_action(GtkToggleAction *action, void *user) -{ - (void) user; - set_zoom_fit(action, ZOOM_FIT_WINDOW); + clear_toggles(GROUP_ZOOM, NULL); + set_zoom_around_centre(normal_zoom); } -void -best_fit_action(GtkToggleAction *action, void *user) -{ - (void) user; - set_zoom_fit(action, ZOOM_BEST_FIT); -} +#define X(E, A, ...)\ + void\ + A(GtkToggleAction *action, void *user)\ + {\ + (void) user;\ + set_zoom_fit(action, E);\ + } +LIST_ZOOM_MODES(X,) +#undef X void next_action(GtkAction *action, void *user) @@ -4,7 +4,12 @@ gbild \- GTK+ 2 Image viewer .SH SYNOPSIS .B gbild -.RB [ \-f ] +.RB [ \-s \ \fIalgorithm\fP] +.RB [ \-S \ \fIsize\fP] +.RB [ \-C \ \fIcolour1\fP[ : \fIcolour2\fP]] +.RB [ \-z \ \fIzoom\fP] +.RB [ -fpctn ] +.RB [ +fpctnC ] .RI [ file "] ..." .SH DESCRIPTION @@ -15,6 +20,93 @@ is a GTK+ 2 image viewer. .TP .B \-f Start in full-screen mode. +.TP +.B +f +Do not start in full-screen mode. +.TP +.B \-p +Show the property pane. +.TP +.B +p +Hide the property pane. +.TP +.B \-c +Show the image collection. +.TP +.B +c +Hide the image collection. +.TP +.B \-t +Apply an image's embedded orientation. +.TP +.B +t +Do not apply an image's embedded orientation. +.TP +.B \-n +Use the image resolution and display resolution to make normal size a physical size. +.TP +.B +n +Use one source pixel per display pixel for normal size. +.TP +.BI \-s \ algorithm +Set the scaling algorithm. Supported values are +.BR nearest , +.BR bilinear , +and +.BR gaussian . +.TP +.BI \-S \ size +Set the chequer square size in pixels. A size of 0 disables the chequered background. +.TP +.BI \-C \ colour1\fR[ : colour2\fR] +Enable the chequered background and set its colours. If the second colour is omitted, the first colour is used for both squares. + +Colours use hexadecimal +.IR RGB , +.IR RRGGBB , +.IR RRRRGGGGBBBB , +.IR RGBA , +.IR RRGGBBAA , +or +.I RRRRGGGGBBBBAAAA +notation, with an optional +.BR # , +.BR 0x , +or +.B 0X +prefix. RGB-only forms may have a leading alpha percentage; +for example, +.B 25%ABCDEF +sets alpha to 25 percent. +.TP +.B +C +Disable the chequered background. +.TP +.BI \-z \ zoom +Set the initial zoom. +.I zoom +shall either be the zoom expressed in perfect (e.g\.\, +.BR 100% ) +or ratio (e.g\.\, +.BR 1:1 ), +or one of the following modes: +.RS +.TP +.B width +Fit to width +.TP +.B height +Fit to height +.TP +.B window +Fit to window +.TP +.B best +Best fit +.TP +.B stretch +Stretch to window +.RE .SH OPERANDS .TP @@ -1,7 +1,7 @@ /* See LICENSE file for copyright and license details. */ #include "common.h" -USAGE("[-f] [file] ..."); +USAGE("[-s algorithm] [-S size] [-C colour[:colour]] [-z zoom] [-fpctn] [+fpctnC] [file] ..."); static int suspend_expose = 0; @@ -18,6 +18,7 @@ format_statusbar_message(char *buffer, size_t size, struct file *file, struct im uintmax_t fsize; size_t unit; int r; + double s; fsize = (uintmax_t)file->size; unit = 0u; @@ -36,12 +37,13 @@ format_statusbar_message(char *buffer, size_t size, struct file *file, struct im fsize /= 10u; done: + s = get_zoom(zoom); r = snprintf(buffer, size, - "Image %zu of %zu | %ju%s %c | %i × %i pixels | %g%% scale", + "Image %zu of %zu | %ju%s %c | %i × %i pixels | %s%g%s scale", current_file + 1u, nfiles, fsize, fsize_decimals, units[unit], image->width, image->height, - 100.0 * get_zoom(zoom)); + s < 1.0 ? "100:" : "", s < 1.0 ? 100.0 / s : 100.0 * s, s < 1.0 ? "" : ":100"); if (r < 0) eprintf("snprintf:"); @@ -128,7 +130,7 @@ draw_transparency_background(cairo_t *cr, struct image *image, double x0, double { cairo_matrix_t matrix; - if (!chequered_background || gtk_toggle_action_get_active(fullscreen)) + if (!chequered_background || chequer_size <= 0 || gtk_toggle_action_get_active(fullscreen)) return; if (!image->vector && !gdk_pixbuf_get_has_alpha(image->raster)) return; @@ -220,7 +222,7 @@ show_image(int update_zoom) 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) + if (update_zoom && zoom_mode != ZOOM_CUSTOM) zoom = calculate_zoom_mode(display_image, zoom_mode); s = get_zoom(zoom); @@ -492,7 +494,7 @@ recalculate_zoom_mode(void *user) { struct image image; (void) user; - if (get_image(&image) && zoom_mode != ZOOM_CUSTOM && zoom_mode != ZOOM_NORMAL) + if (get_image(&image) && zoom_mode != ZOOM_CUSTOM) set_zoom_and_recentre(calculate_zoom_mode(&image, zoom_mode)); return 0; } @@ -509,16 +511,39 @@ int main(int argc, char *argv[]) { int start_fullscreen = 0; + int start_property_pane = 0; + int start_collection_pane = 0; int failed = 0; ARGBEGIN { - case 'f': - start_fullscreen = 1; - break; + case 'f': start_fullscreen = +1; break; + case 'p': start_property_pane = +1; break; + case 'c': start_collection_pane = +1; break; + case 't': apply_embedded_orientation = +1; break; + case 'n': normal_size_is_physical_size = +1; break; + case 'C': if (!parse_and_set_chequer_colours(ARG())) usage(); break; + case 'S': if (!parse_and_set_chequer_size(ARG())) usage(); break; + case 's': if (!parse_and_set_scaling_filter(ARG())) usage(); break; + case 'z': if (!parse_and_set_zoom_mode(ARG())) usage(); break; + default: + usage(); + } ARGALT('+') { + case 'f': start_fullscreen = -1; break; + case 'p': start_property_pane = -1; break; + case 'c': start_collection_pane = -1; break; + case 't': apply_embedded_orientation = -1; break; + case 'n': normal_size_is_physical_size = -1; break; + case 'C': chequered_background = -1; break; default: usage(); } ARGEND; + start_fullscreen = start_fullscreen > 0; + start_property_pane = start_property_pane > 0; + start_collection_pane = start_collection_pane > 0; + apply_embedded_orientation = apply_embedded_orientation > 0; + normal_size_is_physical_size = normal_size_is_physical_size > 0; + if (!set_files_from_argv(argc, argv)) goto fail; @@ -527,6 +552,8 @@ main(int argc, char *argv[]) set_up_ui(); set_up_dragging(); set_up_dropping(); + set_visible_panes(start_property_pane, start_collection_pane); + set_zoom_mode_toggle(); set_actions_sensitive(); gtk_widget_show_all(window); @@ -10,56 +10,60 @@ forget_transformed_raster(void) } int -get_image(struct image *image) +get_image(struct image *image_out) { struct file *file = get_file(); + enum transformation t; 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; + t = apply_embedded_orientation ? multiply_transformations(trans, file->image.embedded_transformation) : trans; + image_out->raster = transformed_raster ? transformed_raster : file->image.raster; + image_out->vector = (t & ~7u) ? NULL : file->image.vector; /* TODO implement support for colour manipulation */ + if (swaps_dimensions(t)) { + image_out->width = file->image.height; + image_out->height = file->image.width; + image_out->has_x_dpi = file->image.has_y_dpi; + image_out->has_y_dpi = file->image.has_x_dpi; + image_out->x_dpi = file->image.y_dpi; + image_out->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; + image_out->width = file->image.width; + image_out->height = file->image.height; + image_out->has_x_dpi = file->image.has_x_dpi; + image_out->has_y_dpi = file->image.has_y_dpi; + image_out->x_dpi = file->image.x_dpi; + image_out->y_dpi = file->image.y_dpi; } - image->has_transformer = get_affine_matrix(&image->transformer, trans, file->image.width, file->image.height); + image_out->embedded_transformation = file->image.embedded_transformation; + image_out->has_transformer = get_affine_matrix(&image_out->transformer, t, 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; + if (t) + image_out->vector = NULL; return 1; } int -get_original_image(struct image *image) +get_original_image(struct image *image_out) { struct file *file = get_file(); 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; + image_out->raster = file->image.raster; + image_out->vector = file->image.vector; + image_out->width = file->image.width; + image_out->height = file->image.height; + image_out->has_x_dpi = file->image.has_x_dpi; + image_out->has_y_dpi = file->image.has_y_dpi; + image_out->embedded_transformation = file->image.embedded_transformation; + image_out->has_transformer = 0; + image_out->x_dpi = file->image.x_dpi; + image_out->y_dpi = file->image.y_dpi; return 1; } @@ -81,6 +81,18 @@ get_raster_dpi_option(GdkPixbuf *raster, const char *name, double *dpi) return 1; } +static int +parse_orientation(const char *s) +{ + char c; + if (!s) return 0; + while (isspace(*s)) s++; + c = *s; + if (!c) return 0; + while (isspace(*s)) s++; + return (!*s || c < '1' || c > '8') ? 0 : (int)c - '0'; +} + int load_image(struct file *file, int errors_to_stderr) { @@ -95,6 +107,7 @@ load_image(struct file *file, int errors_to_stderr) unsigned char *buffer = NULL; size_t size; int fd, ret = 1; + int orientation; if (!file) return 0; @@ -140,11 +153,17 @@ load_image(struct file *file, int errors_to_stderr) file->image.vector = vector; vector = NULL; + if (file->image.vector) { + /* TODO get dimensions and dpi from file->image.vector */ + } 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); + orientation = parse_orientation(gdk_pixbuf_get_option(file->image.raster, "orientation")); + file->image.embedded_transformation = orientation_to_transformation(orientation); + if (file->exif_tags) { g_strfreev(file->exif_tags); file->exif_tags = NULL; diff --git a/preferences.c b/preferences.c index e4d73e2..d1849df 100644 --- a/preferences.c +++ b/preferences.c @@ -13,7 +13,9 @@ GdkColor chequer_colours[2] = { }; uint16_t chequer_alphas[2] = {0xFFFFu, 0xFFFFu}; int chequered_background = 0; -int chequer_size = 16; +int chequer_size = DEFAULT_CHEQUER_SIZE; +int apply_embedded_orientation = 0; +int normal_size_is_physical_size = 0; static GtkWidget *preferences_window; static GtkWidget *transparency_controls; @@ -75,6 +77,33 @@ chequer_size_changed(GtkSpinButton *button, void *user) gtk_widget_queue_draw(display_area); } +static void +apply_embedded_orientation_changed(GtkToggleButton *button, void *user) +{ + struct file *file; + enum transformation t; + int active = gtk_toggle_button_get_active(button); + (void) user; + if (apply_embedded_orientation == active) + return; + apply_embedded_orientation = active; + file = get_file(); + if (file && (t = file->image.embedded_transformation)) + transform_redraw(active ? t : inverse_transformation(t)); + collection_redraw_embedded_orientations(); +} + +static void +normal_size_is_physical_size_changed(GtkToggleButton *button, void *user) +{ + int active = gtk_toggle_button_get_active(button); + (void) user; + if (normal_size_is_physical_size == active) + return; + normal_size_is_physical_size = active; + /* TODO implement */ +} + static GtkWidget * new_section(const char *title) { @@ -117,9 +146,8 @@ new_scaling_section(void) return section; } -#ifdef TODO static GtkWidget * -new_unimplemented_section(void) +new_image_handling_section(void) { GtkWidget *section = new_section("Image handling"); GtkWidget *box = gtk_bin_get_child(GTK_BIN(section)); @@ -128,11 +156,14 @@ new_unimplemented_section(void) orientation = gtk_check_button_new_with_mnemonic("Apply embedded _orientation"); physical_size = gtk_check_button_new_with_mnemonic("Normal size is _physical size"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(orientation), apply_embedded_orientation); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(physical_size), 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); + g_signal_connect(orientation, "toggled", G_CALLBACK(&apply_embedded_orientation_changed), NULL); + g_signal_connect(physical_size, "toggled", G_CALLBACK(&normal_size_is_physical_size_changed), NULL); return section; } -#endif static GtkWidget * new_transparency_section(void) @@ -211,9 +242,7 @@ open_preferences(void) 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_image_handling_section(), 0, 0, 0u); gtk_box_pack_start(GTK_BOX(box), new_transparency_section(), 0, 0, 0u); g_signal_connect(preferences_window, "response", G_CALLBACK(&preferences_response), NULL); @@ -221,3 +250,101 @@ open_preferences(void) gtk_widget_show_all(preferences_window); } + +int +parse_and_set_scaling_filter(const char *s) +{ +#define X(I, V, T) if (!strcasecmp(s, T)) { scaling_filter = V; return 1; } + LIST_SCALING_FILTERS(X, ) +#undef X + return 0; +} + +int +parse_and_set_chequer_size(const char *s) +{ + char *end; + long int size; + errno = 0; + size = strtol(s, &end, 10); + if (errno || end == s || *end || size < 0 || size > INT_MAX) + return 0; + chequer_size = (int)size; + chequered_background = size != 0; + return 1; +} + +static const char * +parse_chequer_colour(const char *s, GdkColor *colour_out, uint16_t *alpha_out) +{ + int has_precent_alpha = 0; + size_t i, j, len, digits, nchannels; + uint16_t mul, channels[] = {0u, 0u, 0u, 65535u}; + + if ('0' <= *s && *s <= '9' && strchr(s, '%')) { + while ('0' <= *s && *s <= '9' && channels[3] < 100u) + channels[3] = (uint16_t)(channels[3] * 10u + (unsigned)(*s++ - '9')); + if (channels[3] > 100u || *s != '%') + return NULL; + s = &s[1]; + channels[3] = (uint16_t)(655.35 * (double)channels[3]); + has_precent_alpha = 1; + } + + if (*s == '#') + s = &s[1u]; + else if (*s == '0' && (s[1u] == 'x' || s[1u] == 'X')) + s = &s[2u]; + + len = strspn(s, "0123456789abcdefABCDEF"); + switch (len) { + case 3u: digits = 1u; mul = 0x1111u; nchannels = 3u; break; + case 4u: digits = 1u; mul = 0x1111u; nchannels = 4u; break; + case 6u: digits = 2u; mul = 0x0101u; nchannels = 3u; break; + case 8u: digits = 2u; mul = 0x0101u; nchannels = 4u; break; + case 12u: digits = 4u; mul = 0x0001u; nchannels = 3u; break; + case 16u: digits = 4u; mul = 0x0001u; nchannels = 4u; break; + default: + return NULL; + } + if (has_precent_alpha && nchannels == 4u) + return NULL; + + for (i = 0u; i < nchannels; i++) { + for (j = 0u; j < digits; j++, s++) { + channels[i] <<= 4; + channels[i] |= (uint16_t)(((int)*s & 15) + (*s > '9' ? 9 : 0)); + } + channels[i] = (uint16_t)(mul * channels[i]); + } + + colour_out->red = channels[0]; + colour_out->green = channels[1]; + colour_out->blue = channels[2]; + *alpha_out = channels[3]; + return s; +} + +int +parse_and_set_chequer_colours(const char *s) +{ + GdkColor colours[2]; + uint16_t alphas[2]; + s = parse_chequer_colour(s, &colours[0u], &alphas[0u]); + if (!s) + return 0; + if (*s == ':') { + s = parse_chequer_colour(&s[1u], &colours[1u], &alphas[1u]); + } else { + colours[1] = colours[0]; + alphas[1] = alphas[0]; + } + if (!s || *s != ':') + return 0; + chequer_colours[0] = colours[0]; + chequer_colours[1] = colours[1]; + chequer_alphas[0] = alphas[0]; + chequer_alphas[1] = alphas[1]; + chequered_background = 1; + return 1; +} diff --git a/properties.c b/properties.c index edf2536..6e1e86c 100644 --- a/properties.c +++ b/properties.c @@ -1,6 +1,8 @@ /* See LICENSE file for copyright and license details. */ #include "common.h" +static const char *svg_units[] = {"%", "px", "em", "ex", "in", "cm", "mm", "pt", "pc", "ch"}; + static GtkWidget * new_metadata_label(const char *text) { @@ -17,12 +19,58 @@ add_metadata_row(GtkTable *table, guint row, PangoAttrList *name_attributes, con GtkWidget *property = gtk_vbox_new(0, 0); GtkWidget *name_label = new_metadata_label(name); GtkWidget *value_label = new_metadata_label(value); + gtk_label_set_line_wrap(GTK_LABEL(name_label), 0); + gtk_label_set_line_wrap(GTK_LABEL(value_label), 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 void +add_thumbnail_row(GtkTable *table, guint row, PangoAttrList *name_attributes, GdkPixbuf *thumbnail) +{ + GtkWidget *property = gtk_vbox_new(0, 0); + GtkWidget *name_label = new_metadata_label("EXIF thumbnail"); + GtkWidget *image = gtk_image_new_from_pixbuf(thumbnail); + gtk_label_set_line_wrap(GTK_LABEL(name_label), 0); + gtk_label_set_attributes(GTK_LABEL(name_label), name_attributes); + gtk_misc_set_alignment(GTK_MISC(image), 0.0, 0.0); + gtk_box_pack_start(GTK_BOX(property), name_label, 0, 0, 0); + gtk_box_pack_start(GTK_BOX(property), image, 0, 0, 0); + gtk_table_attach(table, property, 0, 1, row, row + 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); +} + +static GdkPixbuf * +load_exif_thumbnail(GExiv2Metadata *metadata, enum transformation orientation) +{ + GdkPixbuf *thumbnail = NULL; + GdkPixbufLoader *loader; + GError *error = NULL; + guint8 *data = NULL; + gint size = 0; + + if (!metadata || !gexiv2_metadata_get_exif_thumbnail(metadata, &data, &size) || size <= 0) + goto out; + + loader = gdk_pixbuf_loader_new(); + if (loader && gdk_pixbuf_loader_write(loader, data, (size_t)size, &error) && + gdk_pixbuf_loader_close(loader, &error) && + (thumbnail = gdk_pixbuf_loader_get_pixbuf(loader))) + g_object_ref(thumbnail); + g_clear_error(&error); + g_clear_object(&loader); + +out: + g_free(data); + if (thumbnail && orientation != TRANS_NONE) { + GdkPixbuf *oriented = transform_raster(thumbnail, orientation); + g_object_unref(thumbnail); + thumbnail = oriented; + } + return thumbnail; +} + static int is_unrecognised_metadata(const char *name) { @@ -118,16 +166,9 @@ format_pixel_size(GdkPixbuf *raster) static char * format_orientation(const char *value) { - static const char *const descriptions[] = { - "Normal", - "Flip horizontally", - "Rotate 180°", - "Flip vertically", - "Transpose", - "Rotate 90° clockwise", - "Antitranspose", - "Rotate 90° counterclockwise", - }; +#define X(I, E, T) T + static const char *const descriptions[] = { LIST_ORIENTATIONS(X, COMMA) }; +#undef X char *end, *text; long int orientation = strtol(value, &end, 10); while (isspace((unsigned char)*end)) @@ -157,15 +198,69 @@ format_physical_size(int pixels, double resolution) return text; } +static char * +format_svg_lengths(RsvgLength width, RsvgLength height) +{ + char *text; + double x = width.length; + double y = height.length; + const char *xunit = ""; + const char *yunit = ""; + if (width.unit == RSVG_UNIT_PERCENT) + x *= 100.0; + if (height.unit == RSVG_UNIT_PERCENT) + y *= 100.0; + if ((unsigned)width.unit < ELEMSOF(svg_units)) + xunit = svg_units[width.unit]; + if ((unsigned)height.unit < ELEMSOF(svg_units)) + yunit = svg_units[height.unit]; + if (width.unit == height.unit) { + if (asprintf(&text, "%g × %g %s", x, y, xunit) < 0) + eprintf("asprintf:"); + } else { + if (asprintf(&text, "%g %s × %g %s", x, xunit, y, yunit) < 0) + eprintf("asprintf:"); + } + return text; +} + +static char * +format_svg_length(RsvgLength length) +{ + char *text; + double value = length.length; + const char *unit = ""; + if (length.unit == RSVG_UNIT_PERCENT) + value *= 100.0; + if ((unsigned)length.unit < ELEMSOF(svg_units)) + unit = svg_units[length.unit]; + if (asprintf(&text, "%g %s", value, unit) < 0) + eprintf("asprintf:"); + return text; +} + +static char * +format_svg_viewbox(RsvgRectangle viewbox) +{ + char *text; + if (asprintf(&text, "%g × %g from (%g, %g)", viewbox.width, viewbox.height, viewbox.x, viewbox.y) < 0) + eprintf("asprintf:"); + return text; +} + GtkWidget * create_property_table(struct file *file) { GtkWidget *table; + GdkPixbuf *thumbnail; PangoContext *context; PangoFontMetrics *metrics; PangoAttrList *title_attributes; unsigned i, row = 0u, rows = 2u; const char *orientation = NULL; + RsvgLength svg_width, svg_height; + RsvgRectangle svg_viewbox; + gboolean svg_has_width = 0, svg_has_height = 0, svg_has_viewbox = 0; char *text, *tag, orientation_buf[3u * sizeof(long int) + 1u]; int spacing; GList *keys, *key; @@ -178,6 +273,17 @@ create_property_table(struct file *file) options = gdk_pixbuf_get_options(file->image.raster); + thumbnail = load_exif_thumbnail(file->exif, file->image.embedded_transformation); + rows += thumbnail ? 1u : 0u; + + if (file->image.vector) { + rsvg_handle_get_intrinsic_dimensions(file->image.vector, + &svg_has_width, &svg_width, + &svg_has_height, &svg_height, + &svg_has_viewbox, &svg_viewbox); + rows += (unsigned)((svg_has_width | svg_has_height) + svg_has_viewbox); + } + keys = g_list_sort(g_hash_table_get_keys(options), (GCompareFunc)&strcmp); for (key = keys; key; key = key->next) rows += (unsigned)is_unrecognised_metadata(key->data); @@ -201,7 +307,6 @@ create_property_table(struct file *file) rows += orientation ? 1u : 0u; } - 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)); @@ -215,8 +320,16 @@ create_property_table(struct file *file) ROW("File", file->path); ROW("File size", text = format_file_size(file->size)); + if (svg_has_width && svg_has_height) + ROW("Dimensions", text = format_svg_lengths(svg_width, svg_height)); + else if (svg_has_width) + ROW("Width", text = format_svg_length(svg_width)); + else if (svg_has_height) + ROW("Height", text = format_svg_length(svg_height)); + if (svg_has_viewbox) + ROW("View box", text = format_svg_viewbox(svg_viewbox)); if (file->image.raster && !file->image.vector) - ROW("Pixel dimensions", text = format_pixel_size(file->image.raster)); + ROW("Dimensions", text = format_pixel_size(file->image.raster)); if (orientation) ROW("Orientation", text = format_orientation(orientation)); if (file->image.has_x_dpi) @@ -244,8 +357,12 @@ create_property_table(struct file *file) } } + if (thumbnail) + add_thumbnail_row(GTK_TABLE(table), row++, title_attributes, thumbnail); + #undef ROW + g_clear_object(&thumbnail); pango_attr_list_unref(title_attributes); return table; } diff --git a/transform.c b/transform.c index 4bc4e6e..d58e484 100644 --- a/transform.c +++ b/transform.c @@ -3,6 +3,17 @@ enum transformation trans = TRANS_NONE; +enum transformation +multiply_transformations(enum transformation augment, enum transformation augend) +{ + enum transformation ret = augment; + if (augment & TRANS_TRANSPOSE) + ret -= augend; + else + ret += augend; + return (ret & 3u) | ((augment ^ augend) & ~3u); +} + static GdkPixbuf * make_negative_raster(GdkPixbuf *raster) { @@ -52,11 +63,11 @@ make_negative_raster(GdkPixbuf *raster) } static GdkPixbuf * -apply_rotation_and_reflection(GdkPixbuf *raster) +apply_rotation_and_reflection(enum transformation t, GdkPixbuf *raster) { GdkPixbuf *new_raster, *newer_raster; - switch (trans & 7u) { + switch (t & 7u) { case TRANS_NONE: return NULL; case TRANS_CLOCKWISE: @@ -80,7 +91,7 @@ apply_rotation_and_reflection(GdkPixbuf *raster) if (!new_raster) exit(1); - switch (trans & 7u) { + switch (t & 7u) { case TRANS_TRANSPOSE: case TRANS_ANTITRANSPOSE: newer_raster = gdk_pixbuf_rotate_simple(new_raster, GDK_PIXBUF_ROTATE_CLOCKWISE); @@ -93,29 +104,37 @@ apply_rotation_and_reflection(GdkPixbuf *raster) } } +GdkPixbuf * +transform_raster(GdkPixbuf *raster, enum transformation t) +{ + GdkPixbuf *rotated; + if (t & TRANS_NEGATIVE) + raster = make_negative_raster(raster); + else + g_object_ref(raster); + rotated = apply_rotation_and_reflection(t, raster); + if (!rotated) + return raster; + g_object_unref(raster); + return rotated; +} + int apply_transformations(struct image *image, struct image *image_out) { - GdkPixbuf *negative_raster, *new_raster; + enum transformation t; - if (trans == TRANS_NONE) + t = apply_embedded_orientation ? multiply_transformations(trans, image->embedded_transformation) : trans; + + if (t == 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; - } + if (image->raster) + image_out->raster = transform_raster(image->raster, t); image_out->vector = image->vector; - if (swaps_dimensions(trans)) { + if (swaps_dimensions(t)) { image_out->width = image->height; image_out->height = image->width; image_out->has_x_dpi = image->has_y_dpi; @@ -130,12 +149,13 @@ apply_transformations(struct image *image, struct image *image_out) 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); + image_out->embedded_transformation = image->embedded_transformation; + image_out->has_transformer = get_affine_matrix(&image_out->transformer, t, image->width, image->height); return 1; } -static enum transformation +enum transformation inverse_transformation(enum transformation t) { return ((4u - t) & 3u) | (t & ~3u); @@ -153,10 +173,7 @@ int add_transformation_without_redraw(enum transformation augment) { enum transformation old = trans; - if (augment & TRANS_TRANSPOSE) - trans = ((augment - trans) & 3u) | ((augment ^ trans) & ~3u); - else - trans = ((augment + trans) & 3u) | ((augment ^ trans) & ~3u); + trans = multiply_transformations(augment, trans); return trans != old; } @@ -235,3 +252,15 @@ get_affine_matrix(cairo_matrix_t *matrix_out, enum transformation t, int width, } return 1; } + +enum transformation +orientation_to_transformation(int orientation) +{ + switch (orientation) { +#define X(I, E, T) case I: return E + LIST_ORIENTATIONS(X, ;); + default: + return TRANS_NONE; +#undef X + } +} @@ -30,11 +30,12 @@ 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", "1", "1", normal_size_action, GROUP_ZOOM, 0, D)\ + ACTION(1, 1u, "", "Normal size", "zoom-original", "1", "1", normal_size_action, 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)\ - TOGGLE(1, 1u, "", "Best fit", "zoom-fit-best", "<control>F", "Ctrl+F", best_fit_action, GROUP_ZOOM, 1, D)\ + TOGGLE(1, 1u, "", "Best fit", "zoom-fit-best", "<control>F", "Ctrl+F", best_fit_action, GROUP_ZOOM, 0, D)\ + TOGGLE(0 /* TODO there is no good icon */, 1u, "", "Stretch to window", NULL, "S", "S", stretch_action, GROUP_ZOOM, 0, D)\ TOPLEVEL(1, "", "Go", D)\ ACTION(1, 2u, "", "Previous image", GTK_STOCK_GO_BACK, "<control>Left", "Ctrl+Left", previous_action, D)\ ACTION(1, 2u, "", "Next image", GTK_STOCK_GO_FORWARD, "<control>Right", "Ctrl+Right", next_action, D)\ @@ -204,7 +205,6 @@ set_up_ui(void) 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); @@ -302,6 +302,26 @@ set_fullscreen_mode(int active) } void +set_visible_panes(int property, int collection) +{ + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(get_action("Property pane")), property); + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(get_action("Image collection")), collection); +} + +void +set_zoom_mode_toggle(void) +{ + const char *action = NULL; + +#define X(E, A, O, T, ...) if (zoom_mode == E) action = T + LIST_ZOOM_MODES(X, ;); +#undef X + + if (action) + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(get_action(action)), 1); +} + +void set_property_pane_visible(int visible) { struct file *file; @@ -1,41 +1,71 @@ /* See LICENSE file for copyright and license details. */ #include "common.h" -Zoom zoom = {1.0}; +Zoom zoom = {1.0, INT_MIN}; enum zoom_mode zoom_mode = ZOOM_BEST_FIT; +const Zoom normal_zoom = {1.0, 0}; int zoom_eq(Zoom a, Zoom b) { - return FLOAT_EQ(a, b); + return FLOAT_EQ(a.s, b.s); } double get_zoom(Zoom z) { - return z; + return z.s; } Zoom make_zoom(double s) { - return s; + return (Zoom){s, INT_MIN}; +} + +static Zoom +next_zoom(Zoom z, int dir) +{ + double s = z.s; + int i; + + if (z.i != INT_MIN) { + i = z.i + dir; + goto have_new; + } + +#define LIMIT 70 +#define EXPONENT 1.1 + + s = log(s) / log(EXPONENT); + s = round(s); + i = (int)s; + i += dir; + +have_new: + i = i < -LIMIT ? -LIMIT : i > +LIMIT ? +LIMIT : i; + s = (double)i; + s = pow(EXPONENT, s); + return (Zoom){s, i}; + +#undef LIMIT +#undef EXPONENT } Zoom zoom_in(Zoom z) { - return get_zoom(z) * 1.25; + return next_zoom(z, +1); } Zoom zoom_out(Zoom z) { - return get_zoom(z) * (1.0 / 1.25); + return next_zoom(z, -1); } -Zoom -calculate_fit(struct image *image, int xfit, int yfit, double cap) +static Zoom +calculate_fit(struct image *image, int xfit, int yfit, int maxfit, double cap) { GtkAdjustment *adjustment; double s = 1.0, y; @@ -50,7 +80,7 @@ calculate_fit(struct image *image, int xfit, int yfit, double cap) adjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll)); y = (double)image->height; y = adjustment->page_size / y; - s = xfit ? MIN(s, y) : y; + s = !xfit ? y : maxfit ? MAX(s, y) : MIN(s, y); } if (s > cap) @@ -63,12 +93,53 @@ Zoom 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; +#define X(E, A, O, T, ...) case E: return calculate_fit(image, __VA_ARGS__) + LIST_ZOOM_MODES(X, ;); +#undef X } return zoom; } + +int +parse_and_set_zoom_mode(const char *s) +{ + double a, b, scale; + char *p; + +#define X(E, A, OPT, ...)\ + if (!strcasecmp(s, OPT)) {\ + zoom_mode = E;\ + return 1;\ + } + LIST_ZOOM_MODES(X,); +#undef X + + errno = 0; + if (isspace(*s)) return 0; + a = strtod(s, &p); + if (a <= 0.0 || errno) return 0; + while (*p == ' ') p++; + + if (*p == '%') { + if (*++p) return 0; + scale = 0.01 * a; + } else if (*p == ':') { + while (*++p == ' '); + if (isspace(*p)) return 0; + b = strtod(p, &p); + if (b <= 0.0 || errno) return 0; + scale = a / b; + if (*p) return 0; + } else { + return 0; + } + + if (!isfinite(scale) && scale <= 0.0) + return 0; + + zoom_mode = ZOOM_CUSTOM; + zoom = make_zoom(scale); + return 1; +} |
