Use Little CMS for JPEG colour management
This commit is contained in:
		
							
								
								
									
										33
									
								
								fastiv.c
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										214
									
								
								fiv-io.c
									
									
									
									
									
								
							
							
						
						
									
										214
									
								
								fiv-io.c
									
									
									
									
									
								
							@@ -25,6 +25,11 @@
 | 
			
		||||
#include <spng.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
 | 
			
		||||
#include <jpeglib.h>
 | 
			
		||||
#include <setjmp.h>
 | 
			
		||||
@@ -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  // ---------------------------------------------------------
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								fiv-io.h
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								fiv-io.h
									
									
									
									
									
								
							@@ -21,6 +21,17 @@
 | 
			
		||||
#include <gio/gio.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[];
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										120
									
								
								fiv-view.c
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								fiv-view.c
									
									
									
									
									
								
							@@ -17,6 +17,9 @@
 | 
			
		||||
 | 
			
		||||
#include "config.h"
 | 
			
		||||
 | 
			
		||||
#include "fiv-io.h"
 | 
			
		||||
#include "fiv-view.h"
 | 
			
		||||
 | 
			
		||||
#include <math.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
 | 
			
		||||
@@ -28,9 +31,6 @@
 | 
			
		||||
#include <gdk/gdkquartz.h>
 | 
			
		||||
#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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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())
 | 
			
		||||
 
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user