aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README68
-rw-r--r--TODO27
-rw-r--r--collection.c102
-rw-r--r--common.h64
-rw-r--r--dragging.c79
-rw-r--r--events.c40
-rw-r--r--gbild.194
-rw-r--r--gbild.c45
-rw-r--r--image.c62
-rw-r--r--opening.c19
-rw-r--r--preferences.c141
-rw-r--r--properties.c141
-rw-r--r--transform.c75
-rw-r--r--ui.c26
-rw-r--r--zoom.c101
15 files changed, 894 insertions, 190 deletions
diff --git a/README b/README
index 002c8ce..c005534 100644
--- a/README
+++ b/README
@@ -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
diff --git a/TODO b/TODO
index f0da97e..2890818 100644
--- a/TODO
+++ b/TODO
@@ -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();
+}
diff --git a/common.h b/common.h
index 66462b0..9ae654a 100644
--- a/common.h
+++ b/common.h
@@ -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);
diff --git a/dragging.c b/dragging.c
index 4a2da95..8243b74 100644
--- a/dragging.c
+++ b/dragging.c
@@ -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)
{
diff --git a/events.c b/events.c
index 7a45d66..f4f04cf 100644
--- a/events.c
+++ b/events.c
@@ -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)
diff --git a/gbild.1 b/gbild.1
index ac7734e..29eaa29 100644
--- a/gbild.1
+++ b/gbild.1
@@ -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
diff --git a/gbild.c b/gbild.c
index 79b356a..39a9c41 100644
--- a/gbild.c
+++ b/gbild.c
@@ -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);
diff --git a/image.c b/image.c
index d6b8c0c..db27147 100644
--- a/image.c
+++ b/image.c
@@ -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;
}
diff --git a/opening.c b/opening.c
index 1552b18..54228b1 100644
--- a/opening.c
+++ b/opening.c
@@ -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
+ }
+}
diff --git a/ui.c b/ui.c
index 09edf1e..bc19dfd 100644
--- a/ui.c
+++ b/ui.c
@@ -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;
diff --git a/zoom.c b/zoom.c
index aeae820..284d175 100644
--- a/zoom.c
+++ b/zoom.c
@@ -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;
+}