Use Little CMS for JPEG colour management

This commit is contained in:
Přemysl Eric Janouch 2021-12-22 22:07:49 +01:00
parent 6419209c98
commit 40c1f8327e
Signed by: p
GPG Key ID: A0420B94F92B9493
7 changed files with 313 additions and 79 deletions

View File

@ -229,18 +229,19 @@ make_key_window(void)
XX(S4, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \ XX(S4, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
/* XX(PIN, B("view-pin-symbolic", "Keep view configuration")) */ \ /* XX(PIN, B("view-pin-symbolic", "Keep view configuration")) */ \
/* Or perhaps "blur-symbolic", also in the extended set. */ \ /* 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(SMOOTH, T("blend-tool-symbolic", "Smooth scaling")) \
XX(CHECKERBOARD, T("checkerboard-symbolic", "Highlight transparency")) \ XX(CHECKERBOARD, T("checkerboard-symbolic", "Highlight transparency")) \
XX(ENHANCE, T("heal-symbolic", "Enhance low-quality JPEG")) \ 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(SAVE, B("document-save-as-symbolic", "Save as...")) \
XX(PRINT, B("document-print-symbolic", "Print...")) \ XX(PRINT, B("document-print-symbolic", "Print...")) \
XX(INFO, B("info-symbolic", "Information")) \ 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(LEFT, B("object-rotate-left-symbolic", "Rotate left")) \
XX(MIRROR, B("object-flip-horizontal-symbolic", "Mirror")) \ XX(MIRROR, B("object-flip-horizontal-symbolic", "Mirror")) \
XX(RIGHT, B("object-rotate-right-symbolic", "Rotate right")) \ 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. */ \ /* We are YouTube. */ \
XX(FULLSCREEN, B("view-fullscreen-symbolic", "Fullscreen")) XX(FULLSCREEN, B("view-fullscreen-symbolic", "Fullscreen"))
@ -1067,11 +1068,11 @@ make_view_toolbar(void)
// Exploring different versions of awkward layouts. // Exploring different versions of awkward layouts.
for (int i = 0; i <= TOOLBAR_S1; i++) for (int i = 0; i <= TOOLBAR_S1; i++)
gtk_box_pack_start(box, g.toolbar[i], FALSE, FALSE, 0); 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); gtk_box_pack_end(box, g.toolbar[i], FALSE, FALSE, 0);
GtkWidget *center = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 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_pack_start(GTK_BOX(center), g.toolbar[i], FALSE, FALSE, 0);
gtk_box_set_center_widget(box, center); 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_MINUS, FIV_VIEW_COMMAND_ZOOM_OUT);
toolbar_command(TOOLBAR_ONE, FIV_VIEW_COMMAND_ZOOM_1); toolbar_command(TOOLBAR_ONE, FIV_VIEW_COMMAND_ZOOM_1);
toolbar_toggler(TOOLBAR_FIT, "scale-to-fit"); toolbar_toggler(TOOLBAR_FIT, "scale-to-fit");
toolbar_toggler(TOOLBAR_COLOR, "enable-cms");
toolbar_toggler(TOOLBAR_SMOOTH, "filter"); toolbar_toggler(TOOLBAR_SMOOTH, "filter");
toolbar_toggler(TOOLBAR_CHECKERBOARD, "checkerboard"); toolbar_toggler(TOOLBAR_CHECKERBOARD, "checkerboard");
toolbar_toggler(TOOLBAR_ENHANCE, "enhance"); toolbar_toggler(TOOLBAR_ENHANCE, "enhance");
@ -1107,6 +1109,8 @@ make_view_toolbar(void)
G_CALLBACK(on_notify_view_playing), NULL); G_CALLBACK(on_notify_view_playing), NULL);
g_signal_connect(g.view, "notify::scale-to-fit", g_signal_connect(g.view, "notify::scale-to-fit",
G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_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_signal_connect(g.view, "notify::filter",
G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_SMOOTH]); G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_SMOOTH]);
g_signal_connect(g.view, "notify::checkerboard", 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), "scale");
g_object_notify(G_OBJECT(g.view), "playing"); 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), "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), "filter");
g_object_notify(G_OBJECT(g.view), "checkerboard"); g_object_notify(G_OBJECT(g.view), "checkerboard");
g_object_notify(G_OBJECT(g.view), "enhance"); 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 #ifndef HAVE_JPEG_QS
gtk_widget_set_no_show_all(g.toolbar[TOOLBAR_ENHANCE], TRUE); gtk_widget_set_no_show_all(g.toolbar[TOOLBAR_ENHANCE], TRUE);
#endif #endif
@ -1146,7 +1154,7 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
#toolbar > button:last-child { padding-right: 4px; } \ #toolbar > button:last-child { padding-right: 4px; } \
#toolbar separator { \ #toolbar separator { \
background: mix(@insensitive_fg_color, \ 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 { padding: 5px; } \
fiv-browser.item { \ fiv-browser.item { \
@ -1333,11 +1341,6 @@ main(int argc, char *argv[])
on_toolbar_zoom(NULL, (gpointer) 0); on_toolbar_zoom(NULL, (gpointer) 0);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(funnel), TRUE); 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. // 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., // 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); unit = MAX(200, unit);
gtk_window_set_default_size(GTK_WINDOW(g.window), 4 * unit, 3 * 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_widget_show_all(g.window);
gtk_main(); gtk_main();
return 0; return 0;

214
fiv-io.c
View File

@ -25,6 +25,11 @@
#include <spng.h> #include <spng.h>
#include <turbojpeg.h> #include <turbojpeg.h>
// Colour management must be handled before RGB conversions.
#ifdef HAVE_LCMS2
#include <lcms2.h>
#endif // HAVE_LCMS2
#ifdef HAVE_JPEG_QS #ifdef HAVE_JPEG_QS
#include <jpeglib.h> #include <jpeglib.h>
#include <setjmp.h> #include <setjmp.h>
@ -169,6 +174,129 @@ try_append_page(cairo_surface_t *surface, cairo_surface_t **result,
return true; 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 ------------------------------------------------------------------- // --- Wuffs -------------------------------------------------------------------
// From libwebp, verified to exactly match [x * a / 255]. // From libwebp, verified to exactly match [x * a / 255].
@ -626,31 +754,7 @@ open_wuffs_using(wuffs_base__image_decoder *(*allocate)(),
// --- JPEG -------------------------------------------------------------------- // --- JPEG --------------------------------------------------------------------
static void static GBytes *
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
parse_jpeg_metadata(cairo_surface_t *surface, const gchar *data, gsize len) parse_jpeg_metadata(cairo_surface_t *surface, const gchar *data, gsize len)
{ {
// Because the JPEG file format is simple, just do it manually. // 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 else
g_byte_array_free(exif, TRUE); g_byte_array_free(exif, TRUE);
GBytes *icc_profile = NULL;
if (icc_done) if (icc_done)
cairo_surface_set_user_data(surface, &fiv_io_key_icc, 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); (cairo_destroy_func_t) g_bytes_unref);
else else
g_byte_array_free(icc, TRUE); 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 * 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(); tjhandle dec = tjInitDecompress();
if (!dec) { if (!dec) {
@ -788,18 +917,9 @@ open_libjpeg_turbo(const gchar *data, gsize len, GError **error)
} }
} }
if (pixel_format == TJPF_CMYK) { load_jpeg_finalize(
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with surface, (pixel_format == TJPF_CMYK), profile, data, len);
// 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);
tjDestroy(dec); tjDestroy(dec);
parse_jpeg_metadata(surface, data, len);
return surface; return surface;
} }
@ -824,7 +944,8 @@ libjpeg_error_exit(j_common_ptr cinfo)
} }
static cairo_surface_t * 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; 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) if (cinfo.out_color_space == JCS_CMYK)
trivial_cmyk_to_host_byte_order_argb( trivial_cmyk_to_host_byte_order_argb(
surface_data, cinfo.output_width * cinfo.output_height); surface_data, cinfo.output_width * cinfo.output_height);
cairo_surface_mark_dirty(surface);
(void) jpegqs_finish_decompress(&cinfo); (void) jpegqs_finish_decompress(&cinfo);
load_jpeg_finalize(
surface, (cinfo.out_color_space == JCS_CMYK), profile, data, len);
jpeg_destroy_decompress(&cinfo); jpeg_destroy_decompress(&cinfo);
parse_jpeg_metadata(surface, data, len);
return surface; 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_user_data_key_t fiv_io_key_page_previous;
cairo_surface_t * 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, // 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 // 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; return NULL;
cairo_surface_t *surface = 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); free(data);
return surface; return surface;
} }
cairo_surface_t * cairo_surface_t *
fiv_io_open_from_data(const char *data, size_t len, const gchar *path, 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__slice_u8 prefix =
wuffs_base__make_slice_u8((uint8_t *) data, len); 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; break;
case WUFFS_BASE__FOURCC__JPEG: case WUFFS_BASE__FOURCC__JPEG:
surface = enhance surface = enhance
? open_libjpeg_enhanced(data, len, error) ? open_libjpeg_enhanced(data, len, profile, error)
: open_libjpeg_turbo(data, len, error); : open_libjpeg_turbo(data, len, profile, error);
break; break;
default: default:
#ifdef HAVE_LIBRAW // --------------------------------------------------------- #ifdef HAVE_LIBRAW // ---------------------------------------------------------

View File

@ -21,6 +21,17 @@
#include <gio/gio.h> #include <gio/gio.h>
#include <glib.h> #include <glib.h>
// --- 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[]; extern const char *fiv_io_supported_media_types[];
char **fiv_io_all_supported_media_types(void); 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; extern cairo_user_data_key_t fiv_io_key_page_previous;
cairo_surface_t *fiv_io_open( 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, 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); int fiv_io_filecmp(GFile *f1, GFile *f2);

View File

@ -17,6 +17,9 @@
#include "config.h" #include "config.h"
#include "fiv-io.h"
#include "fiv-view.h"
#include <math.h> #include <math.h>
#include <stdbool.h> #include <stdbool.h>
@ -28,9 +31,6 @@
#include <gdk/gdkquartz.h> #include <gdk/gdkquartz.h>
#endif // GDK_WINDOWING_QUARTZ #endif // GDK_WINDOWING_QUARTZ
#include "fiv-io.h"
#include "fiv-view.h"
struct _FivView { struct _FivView {
GtkWidget parent_instance; GtkWidget parent_instance;
gchar *path; ///< Path to the current image (if any) 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 *page; ///< Current page within image, weak
cairo_surface_t *frame; ///< Current frame within page, weak cairo_surface_t *frame; ///< Current frame within page, weak
FivIoOrientation orientation; ///< Current page orientation FivIoOrientation orientation; ///< Current page orientation
bool filter; ///< Smooth scaling toggle bool enable_cms : 1; ///< Smooth scaling toggle
bool checkerboard; ///< Show checkerboard background bool filter : 1; ///< Smooth scaling toggle
bool enhance; ///< Try to enhance picture data bool checkerboard : 1; ///< Show checkerboard background
bool scale_to_fit; ///< Image no larger than the allocation bool enhance : 1; ///< Try to enhance picture data
bool scale_to_fit : 1; ///< Image no larger than the allocation
double scale; ///< Scaling factor double scale; ///< Scaling factor
cairo_surface_t *enhance_swap; ///< Quick swap in/out 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 int remaining_loops; ///< Greater than zero if limited
gint64 frame_time; ///< Current frame's start, µs precision gint64 frame_time; ///< Current frame's start, µs precision
@ -96,6 +98,7 @@ static FivIoOrientation view_right[9] = {
enum { enum {
PROP_SCALE = 1, PROP_SCALE = 1,
PROP_SCALE_TO_FIT, PROP_SCALE_TO_FIT,
PROP_ENABLE_CMS,
PROP_FILTER, PROP_FILTER,
PROP_CHECKERBOARD, PROP_CHECKERBOARD,
PROP_ENHANCE, PROP_ENHANCE,
@ -113,8 +116,9 @@ static void
fiv_view_finalize(GObject *gobject) fiv_view_finalize(GObject *gobject)
{ {
FivView *self = FIV_VIEW(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->enhance_swap, cairo_surface_destroy);
g_clear_pointer(&self->image, cairo_surface_destroy);
g_free(self->path); g_free(self->path);
G_OBJECT_CLASS(fiv_view_parent_class)->finalize(gobject); G_OBJECT_CLASS(fiv_view_parent_class)->finalize(gobject);
@ -132,6 +136,9 @@ fiv_view_get_property(
case PROP_SCALE_TO_FIT: case PROP_SCALE_TO_FIT:
g_value_set_boolean(value, self->scale_to_fit); g_value_set_boolean(value, self->scale_to_fit);
break; break;
case PROP_ENABLE_CMS:
g_value_set_boolean(value, self->enable_cms);
break;
case PROP_FILTER: case PROP_FILTER:
g_value_set_boolean(value, self->filter); g_value_set_boolean(value, self->filter);
break; break;
@ -173,6 +180,10 @@ fiv_view_set_property(
if (self->scale_to_fit != g_value_get_boolean(value)) if (self->scale_to_fit != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT); fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT);
break; 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: case PROP_FILTER:
if (self->filter != g_value_get_boolean(value)) if (self->filter != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_FILTER); 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]); 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 static void
fiv_view_realize(GtkWidget *widget) fiv_view_realize(GtkWidget *widget)
{ {
@ -363,6 +414,8 @@ fiv_view_realize(GtkWidget *widget)
gtk_widget_register_window(widget, window); gtk_widget_register_window(widget, window);
gtk_widget_set_window(widget, window); gtk_widget_set_window(widget, window);
gtk_widget_set_realized(widget, TRUE); gtk_widget_set_realized(widget, TRUE);
reload_screen_cms_profile(FIV_VIEW(widget), window);
} }
static gboolean static gboolean
@ -1050,15 +1103,18 @@ fiv_view_class_init(FivViewClass *klass)
view_properties[PROP_SCALE_TO_FIT] = g_param_spec_boolean( view_properties[PROP_SCALE_TO_FIT] = g_param_spec_boolean(
"scale-to-fit", "Scale to fit", "Scale images down to fit the window", "scale-to-fit", "Scale to fit", "Scale images down to fit the window",
TRUE, G_PARAM_READWRITE); 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( view_properties[PROP_FILTER] = g_param_spec_boolean(
"filter", "Use filtering", "Scale images smoothly", "filter", "Use filtering", "Scale images smoothly",
TRUE, G_PARAM_READWRITE); TRUE, G_PARAM_READWRITE);
view_properties[PROP_CHECKERBOARD] = g_param_spec_boolean( view_properties[PROP_CHECKERBOARD] = g_param_spec_boolean(
"checkerboard", "Show checkerboard", "Highlight transparent background", "checkerboard", "Show checkerboard", "Highlight transparent background",
TRUE, G_PARAM_READWRITE); FALSE, G_PARAM_READWRITE);
view_properties[PROP_ENHANCE] = g_param_spec_boolean( view_properties[PROP_ENHANCE] = g_param_spec_boolean(
"enhance", "Enhance JPEG", "Enhance low-quality JPEG", "enhance", "Enhance JPEG", "Enhance low-quality JPEG",
TRUE, G_PARAM_READWRITE); FALSE, G_PARAM_READWRITE);
view_properties[PROP_PLAYING] = g_param_spec_boolean( view_properties[PROP_PLAYING] = g_param_spec_boolean(
"playing", "Playing animation", "An animation is running", "playing", "Playing animation", "An animation is running",
FALSE, G_PARAM_READABLE); FALSE, G_PARAM_READABLE);
@ -1099,6 +1155,7 @@ fiv_view_init(FivView *self)
{ {
gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
self->enable_cms = true;
self->filter = true; self->filter = true;
self->checkerboard = false; self->checkerboard = false;
self->scale = 1.0; self->scale = 1.0;
@ -1110,7 +1167,8 @@ fiv_view_init(FivView *self)
gboolean gboolean
fiv_view_open(FivView *self, const gchar *path, GError **error) 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) if (!surface)
return FALSE; return FALSE;
if (self->image) if (self->image)
@ -1157,18 +1215,37 @@ frame_step(FivView *self, int step)
gtk_widget_queue_draw(GTK_WIDGET(self)); 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 static void
swap_enhanced_image(FivView *self) swap_enhanced_image(FivView *self)
{ {
GError *error = NULL; cairo_surface_t *saved = self->image;
cairo_surface_t *surface = self->enhance_swap; self->image = self->page = self->frame = NULL;
if (!surface)
surface = fiv_io_open(self->path, self->enhance, &error); if (self->enhance_swap) {
if (!surface) { switch_page(self, (self->image = self->enhance_swap));
show_error_dialog(get_toplevel(GTK_WIDGET(self)), error); self->enhance_swap = saved;
} else if (reload(self)) {
self->enhance_swap = saved;
} else { } else {
self->enhance_swap = self->image; switch_page(self, (self->image = saved));
switch_page(self, (self->image = surface));
} }
} }
@ -1215,6 +1292,11 @@ fiv_view_command(FivView *self, FivViewCommand command)
? stop_animating(self) ? stop_animating(self)
: start_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: break; case FIV_VIEW_COMMAND_TOGGLE_FILTER:
self->filter = !self->filter; self->filter = !self->filter;
g_object_notify_by_pspec( g_object_notify_by_pspec(

View File

@ -41,6 +41,7 @@ typedef enum _FivViewCommand {
// Going to the end frame makes no sense, wrap around if needed. // Going to the end frame makes no sense, wrap around if needed.
FIV_VIEW_COMMAND_TOGGLE_PLAYBACK, FIV_VIEW_COMMAND_TOGGLE_PLAYBACK,
FIV_VIEW_COMMAND_TOGGLE_CMS,
FIV_VIEW_COMMAND_TOGGLE_FILTER, FIV_VIEW_COMMAND_TOGGLE_FILTER,
FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD, FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD,
FIV_VIEW_COMMAND_TOGGLE_ENHANCE, FIV_VIEW_COMMAND_TOGGLE_ENHANCE,

View File

@ -15,6 +15,7 @@ if get_option('buildtype').startswith('debug')
endif endif
# TODO(p): Use libraw_r later, when we start parallelizing/preloading. # 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')) libraw = dependency('libraw', required : get_option('libraw'))
librsvg = dependency('librsvg-2.0', required : get_option('librsvg')) librsvg = dependency('librsvg-2.0', required : get_option('librsvg'))
xcursor = dependency('xcursor', required : get_option('xcursor')) 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')) gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
dependencies = [ dependencies = [
dependency('gtk+-3.0'), dependency('gtk+-3.0'),
dependency('pixman-1'),
dependency('libturbojpeg'), dependency('libturbojpeg'),
dependency('libjpeg', required : get_option('jpeg-qs')), dependency('libjpeg', required : get_option('jpeg-qs')),
dependency('spng', version : '>=0.7.0', dependency('spng', version : '>=0.7.0',
default_options: 'default_library=static'), default_options: 'default_library=static'),
dependency('pixman-1'),
lcms2,
libraw, libraw,
librsvg, librsvg,
xcursor, xcursor,
@ -50,6 +54,7 @@ conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('PROJECT_VERSION', meson.project_version()) conf.set_quoted('PROJECT_VERSION', meson.project_version())
# TODO(p): Wrap it in a Meson subproject, try to enable SIMD. # 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_JPEG_QS', get_option('jpeg-qs').enabled())
conf.set('HAVE_LCMS2', lcms2.found())
conf.set('HAVE_LIBRAW', libraw.found()) conf.set('HAVE_LIBRAW', libraw.found())
conf.set('HAVE_LIBRSVG', librsvg.found()) conf.set('HAVE_LIBRSVG', librsvg.found())
conf.set('HAVE_XCURSOR', xcursor.found()) conf.set('HAVE_XCURSOR', xcursor.found())

View File

@ -1,3 +1,5 @@
option('lcms2', type : 'feature', value : 'auto',
description : 'Build with Little CMS colour management')
option('jpeg-qs', type : 'feature', value : 'enabled', option('jpeg-qs', type : 'feature', value : 'enabled',
description : 'Build with JPEG Quant Smooth integration') description : 'Build with JPEG Quant Smooth integration')
option('libraw', type : 'feature', value : 'auto', option('libraw', type : 'feature', value : 'auto',