From 40c1f8327e1fdbd48492edd2631ec05c4ade3a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Wed, 22 Dec 2021 22:07:49 +0100 Subject: [PATCH] Use Little CMS for JPEG colour management --- fastiv.c | 33 ++++--- fiv-io.c | 214 ++++++++++++++++++++++++++++++++++++---------- fiv-io.h | 15 +++- fiv-view.c | 120 ++++++++++++++++++++++---- fiv-view.h | 1 + meson.build | 7 +- meson_options.txt | 2 + 7 files changed, 313 insertions(+), 79 deletions(-) diff --git a/fastiv.c b/fastiv.c index ca7cab5..805ed0c 100644 --- a/fastiv.c +++ b/fastiv.c @@ -229,18 +229,19 @@ make_key_window(void) XX(S4, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \ /* XX(PIN, B("view-pin-symbolic", "Keep view configuration")) */ \ /* Or perhaps "blur-symbolic", also in the extended set. */ \ + XX(COLOR, T("preferences-color-symbolic", "Color management")) \ XX(SMOOTH, T("blend-tool-symbolic", "Smooth scaling")) \ XX(CHECKERBOARD, T("checkerboard-symbolic", "Highlight transparency")) \ XX(ENHANCE, T("heal-symbolic", "Enhance low-quality JPEG")) \ - /* XX(COLOR, B("preferences-color-symbolic", "Color management")) */ \ + XX(S5, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \ XX(SAVE, B("document-save-as-symbolic", "Save as...")) \ XX(PRINT, B("document-print-symbolic", "Print...")) \ XX(INFO, B("info-symbolic", "Information")) \ - XX(S5, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \ + XX(S6, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \ XX(LEFT, B("object-rotate-left-symbolic", "Rotate left")) \ XX(MIRROR, B("object-flip-horizontal-symbolic", "Mirror")) \ XX(RIGHT, B("object-rotate-right-symbolic", "Rotate right")) \ - XX(S6, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \ + XX(S7, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \ /* We are YouTube. */ \ XX(FULLSCREEN, B("view-fullscreen-symbolic", "Fullscreen")) @@ -1067,11 +1068,11 @@ make_view_toolbar(void) // Exploring different versions of awkward layouts. for (int i = 0; i <= TOOLBAR_S1; i++) gtk_box_pack_start(box, g.toolbar[i], FALSE, FALSE, 0); - for (int i = TOOLBAR_COUNT; --i >= TOOLBAR_S6; ) + for (int i = TOOLBAR_COUNT; --i >= TOOLBAR_S7; ) gtk_box_pack_end(box, g.toolbar[i], FALSE, FALSE, 0); GtkWidget *center = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - for (int i = TOOLBAR_S1; ++i < TOOLBAR_S6; ) + for (int i = TOOLBAR_S1; ++i < TOOLBAR_S7; ) gtk_box_pack_start(GTK_BOX(center), g.toolbar[i], FALSE, FALSE, 0); gtk_box_set_center_widget(box, center); @@ -1090,6 +1091,7 @@ make_view_toolbar(void) toolbar_command(TOOLBAR_MINUS, FIV_VIEW_COMMAND_ZOOM_OUT); toolbar_command(TOOLBAR_ONE, FIV_VIEW_COMMAND_ZOOM_1); toolbar_toggler(TOOLBAR_FIT, "scale-to-fit"); + toolbar_toggler(TOOLBAR_COLOR, "enable-cms"); toolbar_toggler(TOOLBAR_SMOOTH, "filter"); toolbar_toggler(TOOLBAR_CHECKERBOARD, "checkerboard"); toolbar_toggler(TOOLBAR_ENHANCE, "enhance"); @@ -1107,6 +1109,8 @@ make_view_toolbar(void) G_CALLBACK(on_notify_view_playing), NULL); g_signal_connect(g.view, "notify::scale-to-fit", G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_FIT]); + g_signal_connect(g.view, "notify::enable-cms", + G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_COLOR]); g_signal_connect(g.view, "notify::filter", G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_SMOOTH]); g_signal_connect(g.view, "notify::checkerboard", @@ -1117,10 +1121,14 @@ make_view_toolbar(void) g_object_notify(G_OBJECT(g.view), "scale"); g_object_notify(G_OBJECT(g.view), "playing"); g_object_notify(G_OBJECT(g.view), "scale-to-fit"); + g_object_notify(G_OBJECT(g.view), "enable-cms"); g_object_notify(G_OBJECT(g.view), "filter"); g_object_notify(G_OBJECT(g.view), "checkerboard"); g_object_notify(G_OBJECT(g.view), "enhance"); +#ifndef HAVE_LCMS2 + gtk_widget_set_no_show_all(g.toolbar[TOOLBAR_COLOR], TRUE); +#endif #ifndef HAVE_JPEG_QS gtk_widget_set_no_show_all(g.toolbar[TOOLBAR_ENHANCE], TRUE); #endif @@ -1146,7 +1154,7 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \ #toolbar > button:last-child { padding-right: 4px; } \ #toolbar separator { \ background: mix(@insensitive_fg_color, \ - @insensitive_bg_color, 0.4); margin: 6px 10px; \ + @insensitive_bg_color, 0.4); margin: 6px 8px; \ } \ fiv-browser { padding: 5px; } \ fiv-browser.item { \ @@ -1333,11 +1341,6 @@ main(int argc, char *argv[]) on_toolbar_zoom(NULL, (gpointer) 0); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(funnel), TRUE); - g.files = g_ptr_array_new_full(16, g_free); - g.directory = g_get_current_dir(); - if (!path_arg || !open_any_path(path_arg, browse)) - open_any_path(g.directory, FALSE); - // Try to get half of the screen vertically, in 4:3 aspect ratio. // // We need the GdkMonitor before the GtkWindow has a GdkWindow (i.e., @@ -1355,6 +1358,14 @@ main(int argc, char *argv[]) unit = MAX(200, unit); gtk_window_set_default_size(GTK_WINDOW(g.window), 4 * unit, 3 * unit); + g.files = g_ptr_array_new_full(16, g_free); + g.directory = g_get_current_dir(); + + // XXX: The widget wants to read the display's profile. The realize is ugly. + gtk_widget_realize(g.view); + if (!path_arg || !open_any_path(path_arg, browse)) + open_any_path(g.directory, FALSE); + gtk_widget_show_all(g.window); gtk_main(); return 0; diff --git a/fiv-io.c b/fiv-io.c index 3172582..894330b 100644 --- a/fiv-io.c +++ b/fiv-io.c @@ -25,6 +25,11 @@ #include #include +// Colour management must be handled before RGB conversions. +#ifdef HAVE_LCMS2 +#include +#endif // HAVE_LCMS2 + #ifdef HAVE_JPEG_QS #include #include @@ -169,6 +174,129 @@ try_append_page(cairo_surface_t *surface, cairo_surface_t **result, return true; } +// --- Colour management ------------------------------------------------------- + +FivIoProfile +fiv_io_profile_new(const void *data, size_t len) +{ +#ifdef HAVE_LCMS2 + return cmsOpenProfileFromMem(data, len); +#else + (void) data; + (void) len; + return NULL; +#endif +} + +FivIoProfile +fiv_io_profile_new_sRGB(void) +{ +#ifdef HAVE_LCMS2 + return cmsCreate_sRGBProfile(); +#else + return NULL; +#endif +} + +void +fiv_io_profile_free(FivIoProfile self) +{ +#ifdef HAVE_LCMS2 + cmsCloseProfile(self); +#else + (void) self; +#endif +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F. +#define FIV_IO_LCMS2_ARGB \ + (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8) + +// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with +// ARGB/BGRA/XRGB/BGRX. +static void +trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len) +{ + // This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms. + // Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop + // does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096 + while (len--) { + int c = p[0], m = p[1], y = p[2], k = p[3]; +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + p[0] = k * y / 255; + p[1] = k * m / 255; + p[2] = k * c / 255; + p[3] = 255; +#else + p[3] = k * y / 255; + p[2] = k * m / 255; + p[1] = k * c / 255; + p[0] = 255; +#endif + p += 4; + } +} + +static void +fiv_io_profile_cmyk( + cairo_surface_t *surface, FivIoProfile src, FivIoProfile dst) +{ + unsigned char *data = cairo_image_surface_get_data(surface); + int w = cairo_image_surface_get_width(surface); + int h = cairo_image_surface_get_height(surface); + +#ifndef HAVE_LCMS2 + (void) src; + (void) dst; +#else + cmsHTRANSFORM transform = NULL; + if (src && dst) { + transform = cmsCreateTransform(src, TYPE_CMYK_8_REV, dst, + FIV_IO_LCMS2_ARGB, INTENT_PERCEPTUAL, 0); + } + if (transform) { + cmsDoTransform(transform, data, data, w * h); + cmsDeleteTransform(transform); + return; + } +#endif + trivial_cmyk_to_host_byte_order_argb(data, w * h); +} + +static void +fiv_io_profile_xrgb( + cairo_surface_t *surface, FivIoProfile src, FivIoProfile dst) +{ +#ifndef HAVE_LCMS2 + (void) surface; + (void) src; + (void) dst; +#else + unsigned char *data = cairo_image_surface_get_data(surface); + int w = cairo_image_surface_get_width(surface); + int h = cairo_image_surface_get_height(surface); + + // TODO(p): We should make this optional. + cmsHPROFILE src_fallback = NULL; + if (dst && !src) + src = src_fallback = cmsCreate_sRGBProfile(); + + cmsHTRANSFORM transform = NULL; + if (src && dst) { + transform = cmsCreateTransform(src, FIV_IO_LCMS2_ARGB, dst, + FIV_IO_LCMS2_ARGB, INTENT_PERCEPTUAL, 0); + } + if (transform) { + cmsDoTransform(transform, data, data, w * h); + cmsDeleteTransform(transform); + } + if (src_fallback) + cmsCloseProfile(src_fallback); +#endif +} + // --- Wuffs ------------------------------------------------------------------- // From libwebp, verified to exactly match [x * a / 255]. @@ -626,31 +754,7 @@ open_wuffs_using(wuffs_base__image_decoder *(*allocate)(), // --- JPEG -------------------------------------------------------------------- -static void -trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len) -{ - // Inspired by gdk-pixbuf's io-jpeg.c: - // - // Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop - // does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096 - while (len--) { - int c = p[0], m = p[1], y = p[2], k = p[3]; -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - p[0] = k * y / 255; - p[1] = k * m / 255; - p[2] = k * c / 255; - p[3] = 255; -#else - p[3] = k * y / 255; - p[2] = k * m / 255; - p[1] = k * c / 255; - p[0] = 255; -#endif - p += 4; - } -} - -static void +static GBytes * parse_jpeg_metadata(cairo_surface_t *surface, const gchar *data, gsize len) { // Because the JPEG file format is simple, just do it manually. @@ -732,16 +836,41 @@ parse_jpeg_metadata(cairo_surface_t *surface, const gchar *data, gsize len) else g_byte_array_free(exif, TRUE); + GBytes *icc_profile = NULL; if (icc_done) cairo_surface_set_user_data(surface, &fiv_io_key_icc, - g_byte_array_free_to_bytes(icc), + (icc_profile = g_byte_array_free_to_bytes(icc)), (cairo_destroy_func_t) g_bytes_unref); else g_byte_array_free(icc, TRUE); + return icc_profile; +} + +static void +load_jpeg_finalize(cairo_surface_t *surface, bool cmyk, + FivIoProfile destination, const gchar *data, size_t len) +{ + GBytes *icc_profile = parse_jpeg_metadata(surface, data, len); + FivIoProfile source = NULL; + if (icc_profile) + source = fiv_io_profile_new( + g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile)); + + if (cmyk) + fiv_io_profile_cmyk(surface, source, destination); + else + fiv_io_profile_xrgb(surface, source, destination); + + if (source) + fiv_io_profile_free(source); + + // Pixel data has been written, need to let Cairo know. + cairo_surface_mark_dirty(surface); } static cairo_surface_t * -open_libjpeg_turbo(const gchar *data, gsize len, GError **error) +open_libjpeg_turbo( + const gchar *data, gsize len, FivIoProfile profile, GError **error) { tjhandle dec = tjInitDecompress(); if (!dec) { @@ -788,18 +917,9 @@ open_libjpeg_turbo(const gchar *data, gsize len, GError **error) } } - if (pixel_format == TJPF_CMYK) { - // CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with - // ARGB/BGR/XRGB/BGRX. - trivial_cmyk_to_host_byte_order_argb( - cairo_image_surface_get_data(surface), width * height); - } - - // Pixel data has been written, need to let Cairo know. - cairo_surface_mark_dirty(surface); - + load_jpeg_finalize( + surface, (pixel_format == TJPF_CMYK), profile, data, len); tjDestroy(dec); - parse_jpeg_metadata(surface, data, len); return surface; } @@ -824,7 +944,8 @@ libjpeg_error_exit(j_common_ptr cinfo) } static cairo_surface_t * -open_libjpeg_enhanced(const gchar *data, gsize len, GError **error) +open_libjpeg_enhanced( + const gchar *data, gsize len, FivIoProfile profile, GError **error) { cairo_surface_t *volatile surface = NULL; @@ -880,11 +1001,11 @@ open_libjpeg_enhanced(const gchar *data, gsize len, GError **error) if (cinfo.out_color_space == JCS_CMYK) trivial_cmyk_to_host_byte_order_argb( surface_data, cinfo.output_width * cinfo.output_height); - cairo_surface_mark_dirty(surface); (void) jpegqs_finish_decompress(&cinfo); + load_jpeg_finalize( + surface, (cinfo.out_color_space == JCS_CMYK), profile, data, len); jpeg_destroy_decompress(&cinfo); - parse_jpeg_metadata(surface, data, len); return surface; } @@ -1934,7 +2055,8 @@ cairo_user_data_key_t fiv_io_key_page_next; cairo_user_data_key_t fiv_io_key_page_previous; cairo_surface_t * -fiv_io_open(const gchar *path, gboolean enhance, GError **error) +fiv_io_open( + const gchar *path, FivIoProfile profile, gboolean enhance, GError **error) { // TODO(p): Don't always load everything into memory, test type first, // so that we can reject non-pictures early. Wuffs only needs the first @@ -1956,14 +2078,14 @@ fiv_io_open(const gchar *path, gboolean enhance, GError **error) return NULL; cairo_surface_t *surface = - fiv_io_open_from_data(data, len, path, enhance, error); + fiv_io_open_from_data(data, len, path, profile, enhance, error); free(data); return surface; } cairo_surface_t * fiv_io_open_from_data(const char *data, size_t len, const gchar *path, - gboolean enhance, GError **error) + FivIoProfile profile, gboolean enhance, GError **error) { wuffs_base__slice_u8 prefix = wuffs_base__make_slice_u8((uint8_t *) data, len); @@ -1989,8 +2111,8 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *path, break; case WUFFS_BASE__FOURCC__JPEG: surface = enhance - ? open_libjpeg_enhanced(data, len, error) - : open_libjpeg_turbo(data, len, error); + ? open_libjpeg_enhanced(data, len, profile, error) + : open_libjpeg_turbo(data, len, profile, error); break; default: #ifdef HAVE_LIBRAW // --------------------------------------------------------- diff --git a/fiv-io.h b/fiv-io.h index 21ec0f2..80029ef 100644 --- a/fiv-io.h +++ b/fiv-io.h @@ -21,6 +21,17 @@ #include #include +// --- Colour management ------------------------------------------------------- + +// TODO(p): Make it possible to use Skia's skcms, +// which also supports premultiplied alpha. +typedef void *FivIoProfile; +FivIoProfile fiv_io_profile_new(const void *data, size_t len); +FivIoProfile fiv_io_profile_new_sRGB(void); +void fiv_io_profile_free(FivIoProfile self); + +// --- Loading ----------------------------------------------------------------- + extern const char *fiv_io_supported_media_types[]; char **fiv_io_all_supported_media_types(void); @@ -56,9 +67,9 @@ extern cairo_user_data_key_t fiv_io_key_page_next; extern cairo_user_data_key_t fiv_io_key_page_previous; cairo_surface_t *fiv_io_open( - const gchar *path, gboolean enhance, GError **error); + const gchar *path, FivIoProfile profile, gboolean enhance, GError **error); cairo_surface_t *fiv_io_open_from_data(const char *data, size_t len, - const gchar *path, gboolean enhance, GError **error); + const gchar *path, FivIoProfile profile, gboolean enhance, GError **error); int fiv_io_filecmp(GFile *f1, GFile *f2); diff --git a/fiv-view.c b/fiv-view.c index 2cc77b2..2e50d57 100644 --- a/fiv-view.c +++ b/fiv-view.c @@ -17,6 +17,9 @@ #include "config.h" +#include "fiv-io.h" +#include "fiv-view.h" + #include #include @@ -28,9 +31,6 @@ #include #endif // GDK_WINDOWING_QUARTZ -#include "fiv-io.h" -#include "fiv-view.h" - struct _FivView { GtkWidget parent_instance; gchar *path; ///< Path to the current image (if any) @@ -38,13 +38,15 @@ struct _FivView { cairo_surface_t *page; ///< Current page within image, weak cairo_surface_t *frame; ///< Current frame within page, weak FivIoOrientation orientation; ///< Current page orientation - bool filter; ///< Smooth scaling toggle - bool checkerboard; ///< Show checkerboard background - bool enhance; ///< Try to enhance picture data - bool scale_to_fit; ///< Image no larger than the allocation + bool enable_cms : 1; ///< Smooth scaling toggle + bool filter : 1; ///< Smooth scaling toggle + bool checkerboard : 1; ///< Show checkerboard background + bool enhance : 1; ///< Try to enhance picture data + bool scale_to_fit : 1; ///< Image no larger than the allocation double scale; ///< Scaling factor cairo_surface_t *enhance_swap; ///< Quick swap in/out + FivIoProfile screen_cms_profile; ///< Target colour profile for widget int remaining_loops; ///< Greater than zero if limited gint64 frame_time; ///< Current frame's start, µs precision @@ -96,6 +98,7 @@ static FivIoOrientation view_right[9] = { enum { PROP_SCALE = 1, PROP_SCALE_TO_FIT, + PROP_ENABLE_CMS, PROP_FILTER, PROP_CHECKERBOARD, PROP_ENHANCE, @@ -113,8 +116,9 @@ static void fiv_view_finalize(GObject *gobject) { FivView *self = FIV_VIEW(gobject); - cairo_surface_destroy(self->image); + g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free); g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); + g_clear_pointer(&self->image, cairo_surface_destroy); g_free(self->path); G_OBJECT_CLASS(fiv_view_parent_class)->finalize(gobject); @@ -132,6 +136,9 @@ fiv_view_get_property( case PROP_SCALE_TO_FIT: g_value_set_boolean(value, self->scale_to_fit); break; + case PROP_ENABLE_CMS: + g_value_set_boolean(value, self->enable_cms); + break; case PROP_FILTER: g_value_set_boolean(value, self->filter); break; @@ -173,6 +180,10 @@ fiv_view_set_property( if (self->scale_to_fit != g_value_get_boolean(value)) fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT); break; + case PROP_ENABLE_CMS: + if (self->enable_cms != g_value_get_boolean(value)) + fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_CMS); + break; case PROP_FILTER: if (self->filter != g_value_get_boolean(value)) fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_FILTER); @@ -317,6 +328,46 @@ fiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation) g_object_notify_by_pspec(G_OBJECT(widget), view_properties[PROP_SCALE]); } +// https://www.freedesktop.org/wiki/OpenIcc/ICC_Profiles_in_X_Specification_0.4 +// has disappeared, but you can use the wayback machine. +// +// Note that Wayland does not have any appropriate protocol, as of writing: +// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14 +static void +reload_screen_cms_profile(FivView *self, GdkWindow *window) +{ + g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free); + + GdkDisplay *display = gdk_window_get_display(window); + GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window); + + int num = -1; + for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; ) + if (gdk_display_get_monitor(display, i) == monitor) + num = i; + if (num < 0) + goto out; + + char atom[32] = ""; + g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num); + + // Sadly, there is no nice GTK+/GDK mechanism to watch this for changes. + int format = 0, length = 0; + GdkAtom type = GDK_NONE; + guchar *data = NULL; + GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window)); + if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0, + 8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) { + if (format == 8 && length > 0) + self->screen_cms_profile = fiv_io_profile_new(data, length); + g_free(data); + } + +out: + if (!self->screen_cms_profile) + self->screen_cms_profile = fiv_io_profile_new_sRGB(); +} + static void fiv_view_realize(GtkWidget *widget) { @@ -363,6 +414,8 @@ fiv_view_realize(GtkWidget *widget) gtk_widget_register_window(widget, window); gtk_widget_set_window(widget, window); gtk_widget_set_realized(widget, TRUE); + + reload_screen_cms_profile(FIV_VIEW(widget), window); } static gboolean @@ -1050,15 +1103,18 @@ fiv_view_class_init(FivViewClass *klass) view_properties[PROP_SCALE_TO_FIT] = g_param_spec_boolean( "scale-to-fit", "Scale to fit", "Scale images down to fit the window", TRUE, G_PARAM_READWRITE); + view_properties[PROP_ENABLE_CMS] = g_param_spec_boolean( + "enable-cms", "Enable CMS", "Enable color management", + TRUE, G_PARAM_READWRITE); view_properties[PROP_FILTER] = g_param_spec_boolean( "filter", "Use filtering", "Scale images smoothly", TRUE, G_PARAM_READWRITE); view_properties[PROP_CHECKERBOARD] = g_param_spec_boolean( "checkerboard", "Show checkerboard", "Highlight transparent background", - TRUE, G_PARAM_READWRITE); + FALSE, G_PARAM_READWRITE); view_properties[PROP_ENHANCE] = g_param_spec_boolean( "enhance", "Enhance JPEG", "Enhance low-quality JPEG", - TRUE, G_PARAM_READWRITE); + FALSE, G_PARAM_READWRITE); view_properties[PROP_PLAYING] = g_param_spec_boolean( "playing", "Playing animation", "An animation is running", FALSE, G_PARAM_READABLE); @@ -1099,6 +1155,7 @@ fiv_view_init(FivView *self) { gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); + self->enable_cms = true; self->filter = true; self->checkerboard = false; self->scale = 1.0; @@ -1110,7 +1167,8 @@ fiv_view_init(FivView *self) gboolean fiv_view_open(FivView *self, const gchar *path, GError **error) { - cairo_surface_t *surface = fiv_io_open(path, FALSE, error); + cairo_surface_t *surface = fiv_io_open( + path, self->enable_cms ? self->screen_cms_profile : NULL, FALSE, error); if (!surface) return FALSE; if (self->image) @@ -1157,18 +1215,37 @@ frame_step(FivView *self, int step) gtk_widget_queue_draw(GTK_WIDGET(self)); } +static gboolean +reload(FivView *self) +{ + GError *error = NULL; + cairo_surface_t *surface = fiv_io_open(self->path, + self->enable_cms ? self->screen_cms_profile : NULL, self->enhance, + &error); + if (!surface) { + show_error_dialog(get_toplevel(GTK_WIDGET(self)), error); + return FALSE; + } + + g_clear_pointer(&self->image, cairo_surface_destroy); + g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); + switch_page(self, (self->image = surface)); + return TRUE; +} + static void swap_enhanced_image(FivView *self) { - GError *error = NULL; - cairo_surface_t *surface = self->enhance_swap; - if (!surface) - surface = fiv_io_open(self->path, self->enhance, &error); - if (!surface) { - show_error_dialog(get_toplevel(GTK_WIDGET(self)), error); + cairo_surface_t *saved = self->image; + self->image = self->page = self->frame = NULL; + + if (self->enhance_swap) { + switch_page(self, (self->image = self->enhance_swap)); + self->enhance_swap = saved; + } else if (reload(self)) { + self->enhance_swap = saved; } else { - self->enhance_swap = self->image; - switch_page(self, (self->image = surface)); + switch_page(self, (self->image = saved)); } } @@ -1215,6 +1292,11 @@ fiv_view_command(FivView *self, FivViewCommand command) ? stop_animating(self) : start_animating(self); + break; case FIV_VIEW_COMMAND_TOGGLE_CMS: + self->enable_cms = !self->enable_cms; + g_object_notify_by_pspec( + G_OBJECT(self), view_properties[PROP_ENABLE_CMS]); + reload(self); break; case FIV_VIEW_COMMAND_TOGGLE_FILTER: self->filter = !self->filter; g_object_notify_by_pspec( diff --git a/fiv-view.h b/fiv-view.h index 5f81400..03db6bd 100644 --- a/fiv-view.h +++ b/fiv-view.h @@ -41,6 +41,7 @@ typedef enum _FivViewCommand { // Going to the end frame makes no sense, wrap around if needed. FIV_VIEW_COMMAND_TOGGLE_PLAYBACK, + FIV_VIEW_COMMAND_TOGGLE_CMS, FIV_VIEW_COMMAND_TOGGLE_FILTER, FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD, FIV_VIEW_COMMAND_TOGGLE_ENHANCE, diff --git a/meson.build b/meson.build index de58bd5..f3fae8c 100644 --- a/meson.build +++ b/meson.build @@ -15,6 +15,7 @@ if get_option('buildtype').startswith('debug') endif # TODO(p): Use libraw_r later, when we start parallelizing/preloading. +lcms2 = dependency('lcms2', required : get_option('lcms2')) libraw = dependency('libraw', required : get_option('libraw')) librsvg = dependency('librsvg-2.0', required : get_option('librsvg')) xcursor = dependency('xcursor', required : get_option('xcursor')) @@ -27,11 +28,14 @@ libtiff = dependency('libtiff-4', required : get_option('libtiff')) gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf')) dependencies = [ dependency('gtk+-3.0'), + dependency('pixman-1'), + dependency('libturbojpeg'), dependency('libjpeg', required : get_option('jpeg-qs')), dependency('spng', version : '>=0.7.0', default_options: 'default_library=static'), - dependency('pixman-1'), + + lcms2, libraw, librsvg, xcursor, @@ -50,6 +54,7 @@ conf.set_quoted('PROJECT_NAME', meson.project_name()) conf.set_quoted('PROJECT_VERSION', meson.project_version()) # TODO(p): Wrap it in a Meson subproject, try to enable SIMD. conf.set('HAVE_JPEG_QS', get_option('jpeg-qs').enabled()) +conf.set('HAVE_LCMS2', lcms2.found()) conf.set('HAVE_LIBRAW', libraw.found()) conf.set('HAVE_LIBRSVG', librsvg.found()) conf.set('HAVE_XCURSOR', xcursor.found()) diff --git a/meson_options.txt b/meson_options.txt index 5318848..e8c6f7d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,5 @@ +option('lcms2', type : 'feature', value : 'auto', + description : 'Build with Little CMS colour management') option('jpeg-qs', type : 'feature', value : 'enabled', description : 'Build with JPEG Quant Smooth integration') option('libraw', type : 'feature', value : 'auto',