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