aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2021-12-28 18:51:00 +0100
committerPřemysl Eric Janouch <p@janouch.name>2021-12-28 18:54:27 +0100
commit2d86ffed342b3533561102fb204faa500ca356f7 (patch)
tree59d745349ade276d80386cf623952261c707ce08
parentaaa7cb93c3fb2dc9f47a986c03585dfadad80251 (diff)
downloadfiv-2d86ffed342b3533561102fb204faa500ca356f7.tar.gz
fiv-2d86ffed342b3533561102fb204faa500ca356f7.tar.xz
fiv-2d86ffed342b3533561102fb204faa500ca356f7.zip
Save thumbnails lossily, with metadata
-rw-r--r--fiv-io.c150
-rw-r--r--fiv-io.h7
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,