From 2d4cab52b37c58c881e8bb7786adbe868f004a63 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch
Date: Wed, 22 Dec 2021 10:59:03 +0100
Subject: Integrate jpeg-quantsmooth
Also, don't pointlessly store JPEGs in an ARGB Cairo surface.
---
.gitmodules | 3 +
fastiv.c | 9 +++
fiv-io.c | 122 +++++++++++++++++++++++++++++++++++---
fiv-io.h | 7 ++-
fiv-view.c | 46 +++++++++++++-
fiv-view.h | 1 +
jpeg-quantsmooth | 1 +
meson.build | 3 +
meson_options.txt | 2 +
resources/heal-symbolic.svg | 107 +++++++++++++++++++++++++++++++++
resources/resources.gresource.xml | 1 +
11 files changed, 290 insertions(+), 12 deletions(-)
create mode 160000 jpeg-quantsmooth
create mode 100644 resources/heal-symbolic.svg
diff --git a/.gitmodules b/.gitmodules
index c6b083b..e8c4d71 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "wuffs-mirror-release-c"]
path = wuffs-mirror-release-c
url = https://github.com/google/wuffs-mirror-release-c
+[submodule "jpeg-quantsmooth"]
+ path = jpeg-quantsmooth
+ url = https://github.com/ilyakurdyukov/jpeg-quantsmooth.git
diff --git a/fastiv.c b/fastiv.c
index 9423b88..ca7cab5 100644
--- a/fastiv.c
+++ b/fastiv.c
@@ -231,6 +231,7 @@ make_key_window(void)
/* Or perhaps "blur-symbolic", also in the extended set. */ \
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(SAVE, B("document-save-as-symbolic", "Save as...")) \
XX(PRINT, B("document-print-symbolic", "Print...")) \
@@ -1091,6 +1092,7 @@ make_view_toolbar(void)
toolbar_toggler(TOOLBAR_FIT, "scale-to-fit");
toolbar_toggler(TOOLBAR_SMOOTH, "filter");
toolbar_toggler(TOOLBAR_CHECKERBOARD, "checkerboard");
+ toolbar_toggler(TOOLBAR_ENHANCE, "enhance");
toolbar_command(TOOLBAR_PRINT, FIV_VIEW_COMMAND_PRINT);
toolbar_command(TOOLBAR_SAVE, FIV_VIEW_COMMAND_SAVE_PAGE);
toolbar_command(TOOLBAR_INFO, FIV_VIEW_COMMAND_INFO);
@@ -1109,12 +1111,19 @@ make_view_toolbar(void)
G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_SMOOTH]);
g_signal_connect(g.view, "notify::checkerboard",
G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_CHECKERBOARD]);
+ g_signal_connect(g.view, "notify::enhance",
+ G_CALLBACK(on_notify_view_boolean), g.toolbar[TOOLBAR_ENHANCE]);
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), "filter");
g_object_notify(G_OBJECT(g.view), "checkerboard");
+ g_object_notify(G_OBJECT(g.view), "enhance");
+
+#ifndef HAVE_JPEG_QS
+ gtk_widget_set_no_show_all(g.toolbar[TOOLBAR_ENHANCE], TRUE);
+#endif
GCallback callback = G_CALLBACK(on_view_actions_changed);
g_signal_connect(g.view, "notify::has-image", callback, NULL);
diff --git a/fiv-io.c b/fiv-io.c
index a78d9c9..3172582 100644
--- a/fiv-io.c
+++ b/fiv-io.c
@@ -24,6 +24,16 @@
#include
#include
+
+#ifdef HAVE_JPEG_QS
+#include
+#include
+// This library is tricky to build, simply make it work at all.
+#define NO_SIMD
+#include
+#undef NO_SIMD
+#endif // HAVE_JPEG_QS
+
#ifdef HAVE_LIBRAW
#include
#endif // HAVE_LIBRAW
@@ -159,7 +169,7 @@ try_append_page(cairo_surface_t *surface, cairo_surface_t **result,
return true;
}
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// --- Wuffs -------------------------------------------------------------------
// From libwebp, verified to exactly match [x * a / 255].
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
@@ -614,6 +624,8 @@ open_wuffs_using(wuffs_base__image_decoder *(*allocate)(),
return surface;
}
+// --- JPEG --------------------------------------------------------------------
+
static void
trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
{
@@ -747,10 +759,10 @@ open_libjpeg_turbo(const gchar *data, gsize len, GError **error)
int pixel_format = (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK)
? TJPF_CMYK
- : (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRA : TJPF_ARGB);
+ : (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRX : TJPF_XRGB);
cairo_surface_t *surface =
- cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
cairo_status_t surface_status = cairo_surface_status(surface);
if (surface_status != CAIRO_STATUS_SUCCESS) {
set_error(error, cairo_status_to_string(surface_status));
@@ -791,6 +803,97 @@ open_libjpeg_turbo(const gchar *data, gsize len, GError **error)
return surface;
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#ifdef HAVE_JPEG_QS
+
+struct libjpeg_error_mgr {
+ struct jpeg_error_mgr pub;
+ jmp_buf buf;
+ GError **error;
+};
+
+static void
+libjpeg_error_exit(j_common_ptr cinfo)
+{
+ struct libjpeg_error_mgr *err = (struct libjpeg_error_mgr *) cinfo->err;
+ char buf[JMSG_LENGTH_MAX] = "";
+ (*cinfo->err->format_message)(cinfo, buf);
+ set_error(err->error, buf);
+ longjmp(err->buf, 1);
+}
+
+static cairo_surface_t *
+open_libjpeg_enhanced(const gchar *data, gsize len, GError **error)
+{
+ cairo_surface_t *volatile surface = NULL;
+
+ struct libjpeg_error_mgr jerr = {.error = error};
+ struct jpeg_decompress_struct cinfo = {.err = jpeg_std_error(&jerr.pub)};
+ jerr.pub.error_exit = libjpeg_error_exit;
+ if (setjmp(jerr.buf)) {
+ g_clear_pointer(&surface, cairo_surface_destroy);
+ jpeg_destroy_decompress(&cinfo);
+ return NULL;
+ }
+
+ jpeg_create_decompress(&cinfo);
+ jpeg_mem_src(&cinfo, (const unsigned char *) data, len);
+ (void) jpeg_read_header(&cinfo, true);
+ if (cinfo.jpeg_color_space == JCS_CMYK ||
+ cinfo.jpeg_color_space == JCS_YCCK)
+ cinfo.out_color_space = JCS_CMYK;
+ else if (G_BYTE_ORDER == G_BIG_ENDIAN)
+ cinfo.out_color_space = JCS_EXT_XRGB;
+ else
+ cinfo.out_color_space = JCS_EXT_BGRX;
+
+ jpeg_calc_output_dimensions(&cinfo);
+ int width = cinfo.output_width;
+ int height = cinfo.output_height;
+
+ surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
+ cairo_status_t surface_status = cairo_surface_status(surface);
+ if (surface_status != CAIRO_STATUS_SUCCESS) {
+ set_error(error, cairo_status_to_string(surface_status));
+ longjmp(jerr.buf, 1);
+ }
+
+ unsigned char *surface_data = cairo_image_surface_get_data(surface);
+ int surface_stride = cairo_image_surface_get_stride(surface);
+ JSAMPARRAY lines = (*cinfo.mem->alloc_small)(
+ (j_common_ptr) &cinfo, JPOOL_IMAGE, sizeof *lines * height);
+ for (int i = 0; i < height; i++)
+ lines[i] = surface_data + i * surface_stride;
+
+ // Go for the maximum quality setting.
+ jpegqs_control_t opts = {
+ .flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV | JPEGQS_UPSAMPLE_UV,
+ .threads = g_get_num_processors(),
+ .niter = 3,
+ };
+
+ (void) jpegqs_start_decompress(&cinfo, &opts);
+ while (cinfo.output_scanline < cinfo.output_height)
+ (void) jpeg_read_scanlines(&cinfo, lines + cinfo.output_scanline,
+ cinfo.output_height - cinfo.output_scanline);
+ 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);
+
+ jpeg_destroy_decompress(&cinfo);
+ parse_jpeg_metadata(surface, data, len);
+ return surface;
+}
+
+#else
+#define open_libjpeg_enhanced open_libjpeg_turbo
+#endif
+
+// --- Optional dependencies ---------------------------------------------------
+
#ifdef HAVE_LIBRAW // ---------------------------------------------------------
static cairo_surface_t *
@@ -1831,7 +1934,7 @@ 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, GError **error)
+fiv_io_open(const gchar *path, 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
@@ -1852,14 +1955,15 @@ fiv_io_open(const gchar *path, GError **error)
if (!g_file_get_contents(path, &data, &len, error))
return NULL;
- cairo_surface_t *surface = fiv_io_open_from_data(data, len, path, error);
+ cairo_surface_t *surface =
+ fiv_io_open_from_data(data, len, path, enhance, error);
free(data);
return surface;
}
cairo_surface_t *
-fiv_io_open_from_data(
- const char *data, size_t len, const gchar *path, GError **error)
+fiv_io_open_from_data(const char *data, size_t len, const gchar *path,
+ gboolean enhance, GError **error)
{
wuffs_base__slice_u8 prefix =
wuffs_base__make_slice_u8((uint8_t *) data, len);
@@ -1884,7 +1988,9 @@ fiv_io_open_from_data(
error);
break;
case WUFFS_BASE__FOURCC__JPEG:
- surface = open_libjpeg_turbo(data, len, error);
+ surface = enhance
+ ? open_libjpeg_enhanced(data, len, error)
+ : open_libjpeg_turbo(data, len, error);
break;
default:
#ifdef HAVE_LIBRAW // ---------------------------------------------------------
diff --git a/fiv-io.h b/fiv-io.h
index 5fbe276..21ec0f2 100644
--- a/fiv-io.h
+++ b/fiv-io.h
@@ -55,9 +55,10 @@ extern cairo_user_data_key_t fiv_io_key_page_next;
/// There is no wrap-around. This is a weak pointer.
extern cairo_user_data_key_t fiv_io_key_page_previous;
-cairo_surface_t *fiv_io_open(const gchar *path, GError **error);
-cairo_surface_t *fiv_io_open_from_data(
- const char *data, size_t len, const gchar *path, GError **error);
+cairo_surface_t *fiv_io_open(
+ const gchar *path, 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);
int fiv_io_filecmp(GFile *f1, GFile *f2);
diff --git a/fiv-view.c b/fiv-view.c
index b330b26..cd1f5cd 100644
--- a/fiv-view.c
+++ b/fiv-view.c
@@ -40,9 +40,12 @@ struct _FivView {
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
double scale; ///< Scaling factor
+ cairo_surface_t *enhance_swap; ///< Quick swap in/out
+
int remaining_loops; ///< Greater than zero if limited
gint64 frame_time; ///< Current frame's start, µs precision
gulong frame_update_connection; ///< GdkFrameClock::update
@@ -95,6 +98,7 @@ enum {
PROP_SCALE_TO_FIT,
PROP_FILTER,
PROP_CHECKERBOARD,
+ PROP_ENHANCE,
PROP_PLAYING,
PROP_HAS_IMAGE,
PROP_CAN_ANIMATE,
@@ -110,6 +114,7 @@ fiv_view_finalize(GObject *gobject)
{
FivView *self = FIV_VIEW(gobject);
cairo_surface_destroy(self->image);
+ g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
g_free(self->path);
G_OBJECT_CLASS(fiv_view_parent_class)->finalize(gobject);
@@ -133,6 +138,9 @@ fiv_view_get_property(
case PROP_CHECKERBOARD:
g_value_set_boolean(value, self->checkerboard);
break;
+ case PROP_ENHANCE:
+ g_value_set_boolean(value, self->enhance);
+ break;
case PROP_PLAYING:
g_value_set_boolean(value, !!self->frame_update_connection);
break;
@@ -173,6 +181,10 @@ fiv_view_set_property(
if (self->checkerboard != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD);
break;
+ case PROP_ENHANCE:
+ if (self->enhance != g_value_get_boolean(value))
+ fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_ENHANCE);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
@@ -1044,6 +1056,9 @@ fiv_view_class_init(FivViewClass *klass)
view_properties[PROP_CHECKERBOARD] = g_param_spec_boolean(
"checkerboard", "Show checkerboard", "Highlight transparent background",
TRUE, G_PARAM_READWRITE);
+ view_properties[PROP_ENHANCE] = g_param_spec_boolean(
+ "enhance", "Enhance JPEG", "Enhance low-quality JPEG",
+ TRUE, G_PARAM_READWRITE);
view_properties[PROP_PLAYING] = g_param_spec_boolean(
"playing", "Playing animation", "An animation is running",
FALSE, G_PARAM_READABLE);
@@ -1095,12 +1110,20 @@ fiv_view_init(FivView *self)
gboolean
fiv_view_open(FivView *self, const gchar *path, GError **error)
{
- cairo_surface_t *surface = fiv_io_open(path, error);
+ cairo_surface_t *surface = fiv_io_open(path, self->enhance, error);
if (!surface)
return FALSE;
if (self->image)
cairo_surface_destroy(self->image);
+ // This is extremely expensive, and only works sometimes.
+ g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
+ if (self->enhance) {
+ self->enhance = FALSE;
+ g_object_notify_by_pspec(
+ G_OBJECT(self), view_properties[PROP_ENHANCE]);
+ }
+
self->frame = self->page = NULL;
self->image = surface;
switch_page(self, self->image);
@@ -1134,6 +1157,21 @@ frame_step(FivView *self, int step)
gtk_widget_queue_draw(GTK_WIDGET(self));
}
+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);
+ } else {
+ self->enhance_swap = self->image;
+ switch_page(self, (self->image = surface));
+ }
+}
+
void
fiv_view_command(FivView *self, FivViewCommand command)
{
@@ -1187,6 +1225,12 @@ fiv_view_command(FivView *self, FivViewCommand command)
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_CHECKERBOARD]);
gtk_widget_queue_draw(widget);
+ break; case FIV_VIEW_COMMAND_TOGGLE_ENHANCE:
+ self->enhance = !self->enhance;
+ g_object_notify_by_pspec(
+ G_OBJECT(self), view_properties[PROP_ENHANCE]);
+ swap_enhanced_image(self);
+
break; case FIV_VIEW_COMMAND_PRINT:
print(self);
break; case FIV_VIEW_COMMAND_SAVE_PAGE:
diff --git a/fiv-view.h b/fiv-view.h
index ea27e27..5f81400 100644
--- a/fiv-view.h
+++ b/fiv-view.h
@@ -43,6 +43,7 @@ typedef enum _FivViewCommand {
FIV_VIEW_COMMAND_TOGGLE_FILTER,
FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD,
+ FIV_VIEW_COMMAND_TOGGLE_ENHANCE,
FIV_VIEW_COMMAND_PRINT,
FIV_VIEW_COMMAND_SAVE_PAGE,
FIV_VIEW_COMMAND_INFO,
diff --git a/jpeg-quantsmooth b/jpeg-quantsmooth
new file mode 160000
index 0000000..c86c641
--- /dev/null
+++ b/jpeg-quantsmooth
@@ -0,0 +1 @@
+Subproject commit c86c6418ea6c827513d206694847033f9ca50151
diff --git a/meson.build b/meson.build
index 0cabcc4..de58bd5 100644
--- a/meson.build
+++ b/meson.build
@@ -28,6 +28,7 @@ gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
dependencies = [
dependency('gtk+-3.0'),
dependency('libturbojpeg'),
+ dependency('libjpeg', required : get_option('jpeg-qs')),
dependency('spng', version : '>=0.7.0',
default_options: 'default_library=static'),
dependency('pixman-1'),
@@ -47,6 +48,8 @@ dependencies = [
conf = configuration_data()
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_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 97e393b..5318848 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,5 @@
+option('jpeg-qs', type : 'feature', value : 'enabled',
+ description : 'Build with JPEG Quant Smooth integration')
option('libraw', type : 'feature', value : 'auto',
description : 'Build with raw photo support, requires LibRaw')
option('librsvg', type : 'feature', value : 'auto',
diff --git a/resources/heal-symbolic.svg b/resources/heal-symbolic.svg
new file mode 100644
index 0000000..4d488c2
--- /dev/null
+++ b/resources/heal-symbolic.svg
@@ -0,0 +1,107 @@
+
+
diff --git a/resources/resources.gresource.xml b/resources/resources.gresource.xml
index 1a99827..45439b6 100644
--- a/resources/resources.gresource.xml
+++ b/resources/resources.gresource.xml
@@ -5,5 +5,6 @@
funnel-symbolic.svg
blend-tool-symbolic.svg
checkerboard-symbolic.svg
+ heal-symbolic.svg
--
cgit v1.2.3-70-g09d2