From 2d86ffed342b3533561102fb204faa500ca356f7 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch <p@janouch.name>
Date: Tue, 28 Dec 2021 18:51:00 +0100
Subject: Save thumbnails lossily, with metadata

---
 fiv-io.c | 150 ++++++++++++++++++++++++++++++++++++++++++++++-----------------
 fiv-io.h |   7 +++
 2 files changed, 118 insertions(+), 39 deletions(-)

diff --git a/fiv-io.c b/fiv-io.c
index 0126d2b..dfea508 100644
--- a/fiv-io.c
+++ b/fiv-io.c
@@ -2440,8 +2440,9 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *path,
 
 // --- Export ------------------------------------------------------------------
 
-static WebPData
-encode_lossless_webp(cairo_surface_t *surface)
+unsigned char *
+fiv_io_encode_webp(
+	cairo_surface_t *surface, const WebPConfig *config, size_t *len)
 {
 	cairo_format_t format = cairo_image_surface_get_format(surface);
 	int w = cairo_image_surface_get_width(surface);
@@ -2460,15 +2461,10 @@ encode_lossless_webp(cairo_surface_t *surface)
 		surface = cairo_surface_reference(surface);
 	}
 
-	WebPConfig config = {};
+	WebPMemoryWriter writer = {};
+	WebPMemoryWriterInit(&writer);
 	WebPPicture picture = {};
-	if (!WebPConfigInit(&config) ||
-		!WebPConfigLosslessPreset(&config, 6) ||
-		!WebPPictureInit(&picture))
-		goto fail;
-
-	config.thread_level = true;
-	if (!WebPValidateConfig(&config))
+	if (!WebPPictureInit(&picture))
 		goto fail;
 
 	picture.use_argb = true;
@@ -2495,18 +2491,33 @@ encode_lossless_webp(cairo_surface_t *surface)
 		for (int i = h * picture.argb_stride; i-- > 0; argb++)
 			*argb |= 0xFF000000;
 
-	WebPMemoryWriter writer = {};
-	WebPMemoryWriterInit(&writer);
 	picture.writer = WebPMemoryWrite;
 	picture.custom_ptr = &writer;
-	if (!WebPEncode(&config, &picture))
+	if (!WebPEncode(config, &picture))
 		g_debug("WebPEncode: %d\n", picture.error_code);
 
 fail_compatibility:
 	WebPPictureFree(&picture);
 fail:
 	cairo_surface_destroy(surface);
-	return (WebPData) {.bytes = writer.mem, .size = writer.size};
+	*len = writer.size;
+	return writer.mem;
+}
+
+static WebPData
+encode_lossless_webp(cairo_surface_t *surface)
+{
+	WebPData bitstream = {};
+	WebPConfig config = {};
+	if (!WebPConfigInit(&config) || !WebPConfigLosslessPreset(&config, 6))
+		return bitstream;
+
+	config.thread_level = true;
+	if (!WebPValidateConfig(&config))
+		return bitstream;
+
+	bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size);
+	return bitstream;
 }
 
 static gboolean
@@ -2825,6 +2836,68 @@ rescale_thumbnail(cairo_surface_t *thumbnail, double row_height)
 	return scaled;
 }
 
+static WebPData
+encode_thumbnail(cairo_surface_t *surface)
+{
+	WebPData bitstream = {};
+	WebPConfig config = {};
+	if (!WebPConfigInit(&config) || !WebPConfigLosslessPreset(&config, 6))
+		return bitstream;
+
+	config.near_lossless = 95;
+	config.thread_level = true;
+	if (!WebPValidateConfig(&config))
+		return bitstream;
+
+	bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size);
+	return bitstream;
+}
+
+static void
+save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
+{
+	WebPMux *mux = WebPMuxNew();
+	WebPData bitstream = encode_thumbnail(thumbnail);
+	gboolean ok = WebPMuxSetImage(mux, &bitstream, true) == WEBP_MUX_OK;
+	WebPDataClear(&bitstream);
+
+	WebPData data = {.bytes = (const uint8_t *) thum->str, .size = thum->len};
+	ok = ok && WebPMuxSetChunk(mux, "THUM", &data, false) == WEBP_MUX_OK;
+
+	WebPData assembled = {};
+	WebPDataInit(&assembled);
+	ok = ok && WebPMuxAssemble(mux, &assembled) == WEBP_MUX_OK;
+	WebPMuxDelete(mux);
+	if (!ok) {
+		g_warning("thumbnail encoding failed");
+		return;
+	}
+
+	GError *e = NULL;
+	while (!g_file_set_contents(
+		path, (const gchar *) assembled.bytes, assembled.size, &e)) {
+		bool missing_parents =
+			e->domain == G_FILE_ERROR && e->code == G_FILE_ERROR_NOENT;
+		g_debug("%s: %s", path, e->message);
+		g_clear_error(&e);
+		if (!missing_parents)
+			break;
+
+		gchar *dirname = g_path_get_dirname(path);
+		int err = g_mkdir_with_parents(dirname, 0755);
+		if (err)
+			g_debug("%s: %s", dirname, g_strerror(errno));
+
+		g_free(dirname);
+		if (err)
+			break;
+	}
+
+	// It would be possible to create square thumbnails as well,
+	// but it seems like wasted effort.
+	WebPDataClear(&assembled);
+}
+
 gboolean
 fiv_io_produce_thumbnail(GFile *target, FivIoThumbnailSize size, GError **error)
 {
@@ -2851,9 +2924,9 @@ fiv_io_produce_thumbnail(GFile *target, FivIoThumbnailSize size, GError **error)
 
 	// TODO(p): Add a flag to avoid loading all pages and frames.
 	FivIoProfile sRGB = fiv_io_profile_new_sRGB();
-	cairo_surface_t *surface =
-		fiv_io_open_from_data(g_mapped_file_get_contents(mf),
-			g_mapped_file_get_length(mf), path, sRGB, FALSE, error);
+	gsize filesize = g_mapped_file_get_length(mf);
+	cairo_surface_t *surface = fiv_io_open_from_data(
+		g_mapped_file_get_contents(mf), filesize, path, sRGB, FALSE, error);
 
 	g_free(path);
 	g_mapped_file_unref(mf);
@@ -2867,37 +2940,36 @@ fiv_io_produce_thumbnail(GFile *target, FivIoThumbnailSize size, GError **error)
 	gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
 	gchar *thumbnails_dir = fiv_io_get_thumbnail_root();
 
+	GString *thum = g_string_new("");
+	g_string_append_printf(
+		thum, "%s%c%s%c", "Thumb::URI", 0, uri, 0);
+	g_string_append_printf(
+		thum, "%s%c%ld%c", "Thumb::Mtime", 0, (long) st.st_mtim.tv_sec, 0);
+	g_string_append_printf(
+		thum, "%s%c%ld%c", "Thumb::Size", 0, (long) filesize, 0);
+	g_string_append_printf(thum, "%s%c%d%c", "Thumb::Image::Width", 0,
+		cairo_image_surface_get_width(surface), 0);
+	g_string_append_printf(thum, "%s%c%d%c", "Thumb::Image::Height", 0,
+		cairo_image_surface_get_height(surface), 0);
+
+	// Without a CMM, no conversion is attempted.
+	if (sRGB) {
+		g_string_append_printf(
+			thum, "%s%c%s%c", "Thumb::ColorSpace", 0, "sRGB", 0);
+	}
+
 	for (int use = size; use >= FIV_IO_THUMBNAIL_SIZE_MIN; use--) {
 		cairo_surface_t *scaled =
 			rescale_thumbnail(surface, fiv_io_thumbnail_sizes[use].size);
 		gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir,
 			fiv_io_thumbnail_sizes[use].thumbnail_spec_name, sum);
-
-		GError *e = NULL;
-		while (!fiv_io_save(scaled, scaled, NULL, path, &e)) {
-			bool missing_parents =
-				e->domain == G_FILE_ERROR && e->code == G_FILE_ERROR_NOENT;
-			g_debug("%s: %s", path, e->message);
-			g_clear_error(&e);
-			if (!missing_parents)
-				break;
-
-			gchar *dirname = g_path_get_dirname(path);
-			int err = g_mkdir_with_parents(dirname, 0755);
-			if (err)
-				g_debug("%s: %s", dirname, g_strerror(errno));
-
-			g_free(dirname);
-			if (err)
-				break;
-		}
-
-		// It would be possible to create square thumbnails as well,
-		// but it seems like wasted effort.
+		save_thumbnail(scaled, path, thum);
 		cairo_surface_destroy(scaled);
 		g_free(path);
 	}
 
+	g_string_free(thum, TRUE);
+
 	g_free(thumbnails_dir);
 	g_free(sum);
 	g_free(uri);
diff --git a/fiv-io.h b/fiv-io.h
index 97f4fa7..b561286 100644
--- a/fiv-io.h
+++ b/fiv-io.h
@@ -79,6 +79,13 @@ int fiv_io_filecmp(GFile *f1, GFile *f2);
 
 // --- Export ------------------------------------------------------------------
 
+typedef struct WebPConfig WebPConfig;
+
+/// Encodes a Cairo surface as a WebP bitstream, following the configuration.
+/// The result needs to be freed using WebPFree/WebPDataClear().
+unsigned char *fiv_io_encode_webp(
+	cairo_surface_t *surface, const WebPConfig *config, size_t *len);
+
 /// Saves the page as a lossless WebP still picture or animation.
 /// If no exact frame is specified, this potentially creates an animation.
 gboolean fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame,
-- 
cgit v1.2.3-70-g09d2