Compare commits

...

65 Commits

Author SHA1 Message Date
2234fd008d MSYS2: add a comment about realpath
All checks were successful
Alpine 3.21 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-03 18:05:41 +01:00
0fceaf7728 Bump Wuffs
All checks were successful
Alpine 3.21 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-02 02:09:28 +01:00
c46fc73c34 Prefill the 'Enter location' dialog 2025-11-02 02:09:28 +01:00
bdd18fc898 Very slightly improve file updates on macOS
Some checks failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
openSUSE 15.5 Success
OpenBSD 7.7 Success
OpenBSD 7.6 Unsupported
Alpine 3.21 Success
2025-10-18 17:47:25 +01:00
cf6ded1d03 Make browser Cmd+click open new windows on macOS
Some checks failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.6 Scripts failed
openSUSE 15.5 Success
Alpine 3.21 Success
2025-10-18 15:24:36 +01:00
3bea18708f Bump version, update README.adoc
All checks were successful
Alpine 3.20 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.5 Success
openSUSE 15.5 Success
2024-12-23 16:53:54 +01:00
ed8ba147ba Improve packaging directory structure
All checks were successful
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.5 Success
openSUSE 15.5 Success
Alpine 3.20 Success
2024-12-23 16:53:54 +01:00
c221a00c33 Improve MSI package names
All checks were successful
Alpine 3.20 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.5 Success
openSUSE 15.5 Success
2024-12-23 16:12:35 +01:00
192ffa0de9 Update a comment 2024-07-27 08:43:56 +02:00
bac9fce4e0 Fix argument order in g_malloc0_n() usages
Some checks failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.5 Success
openSUSE 15.5 Success
Alpine 3.20 Scripts failed
2024-07-10 00:30:27 +02:00
2e9ea9b4e2 Do not rely on a particular CWD on Windows
on_app_activate() currently makes use of the CWD we are launched with,
so I'm choosing to not enforce it globally.
2024-07-10 00:29:49 +02:00
b34fe63198 Fix reverse image search
All checks were successful
Alpine 3.19 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.3 Success
openSUSE 15.5 Success
It was only a matter of time before this would fail,
although I did not expect this to happen so soon.
2024-04-22 07:38:49 +02:00
3c8ddcaf26 Fix high-DPI scaling with OpenGL
All checks were successful
Alpine 3.19 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.3 Success
openSUSE 15.5 Success
We used to render multiple copies (four for a scaling factor of 2).
2024-04-13 05:16:48 +02:00
e3ec07a19f Improve cross-compilation script compatibility
All checks were successful
Arch Linux AUR Success
Arch Linux Success
Alpine 3.19 Success
Debian Bookworm Success
OpenBSD 7.3 Success
Fedora 39 Success
openSUSE 15.5 Success
2024-04-07 01:06:46 +02:00
e57364cd97 Fix openSUSE 15.5 and Win32 builds 2024-04-06 23:56:47 +02:00
7330f07dd7 Fix LibRaw 0.20 compatibility 2024-03-28 16:03:40 +01:00
d68e09525c Update the screenshot
Taken on Ubuntu 23.10.  Unfortunately, on this distribution,
the dark mode of the theme doesn't apply to window titles.

The GNOME Shell's screenshot tool captures window shadows without
the background, and it can be used on unfocused windows as well.
2024-03-21 03:57:17 +01:00
115a7bab0f Fix a build issue, and a big endian conversion 2024-03-13 18:47:05 +01:00
91538aaba5 Add an experimental OpenGL renderer 2024-03-13 15:27:31 +01:00
c214e668d9 Resolve more GLib #2907 warnings 2024-02-24 00:54:29 +01:00
a5ebc697ad Do not restart all thumbnailers on new entries
This had the potential to create tons of unnecessary processes
doing the same job.

The change only covers moving or linking, not copying.
2024-01-30 02:34:05 +01:00
9ca18f52d5 Clean up thumbnailing 2024-01-30 02:16:17 +01:00
604594a8f1 Prepare for parallelized colour management
This rewrite is more or less necessary for:
 - colour-managed browser thumbnails,
 - asynchronous image loading,
 - turning fiv-io into a reusable library.

Little CMS has a fairly terrible API in this regard.
2024-01-28 01:48:28 +01:00
9acab00bcc Improve browser view styling 2024-01-26 21:00:30 +01:00
ae8dc3070a Partially circumvent a Little CMS bug 2024-01-26 19:55:31 +01:00
3c8a280546 Move colour management to its own compilation unit
Also make it apparent that CMM profiles are pointer types.

This isn't all that pretty, but it's a necessary first step.
2024-01-26 19:17:54 +01:00
96189b70b8 Mark places where lcms2 should use contexts 2024-01-26 17:25:04 +01:00
67433f3776 Add a --collection toggle
One possible use of it is to avoid thumbnailing the parent directory.
2024-01-26 16:57:36 +01:00
c1418c7462 Decrease sidebar padding
Nothing fits in there normally, it's about time to acknowledge that.
2024-01-26 16:38:22 +01:00
935506b120 Make the Delete key move files to trash in browser 2024-01-26 16:37:29 +01:00
84269b2ba2 Load AdobeRGB Nikon JPEGs correctly 2024-01-23 22:18:17 +01:00
51ca3f8e2e info: optionally recurse into certain MakerNotes 2024-01-23 19:12:11 +01:00
f196b03e97 Resolve warnings resulting from GLib #2907 2024-01-22 12:45:26 +01:00
ee08565389 Resolve spurious overshoot indicators
_gtk_scrolled_window_get_overshoot() decrements the page size
from the upper value before using it for comparisons.
2023-12-28 11:22:17 +01:00
c04c4063e4 Fix a class of animated transparent WebPs 2023-12-28 07:48:11 +01:00
aed6ae6b83 Add a comment regarding high-precision JPEGs 2023-12-05 04:57:01 +01:00
bae640a116 Circumvent JPEG QS & libjpeg-turbo incompatibility
UV upsampling visibly requires JPEG QS to update its code
to follow recent changes within libjpeg-turbo.
2023-12-05 03:35:33 +01:00
52c17c8a16 Bump JPEG Quant Smooth 2023-12-05 00:28:28 +01:00
b07fba0c9c Make multi-monitor CM work better with xiccd
Let's assume the profile it picks is appropriate for all monitors.
2023-10-17 15:34:44 +02:00
72bf913f3d Add a tool to find hot pixels
It works well for my Nikon.

Note that hot pixels can be eliminated in the camera itself,
when you run sensor cleaning immediately after a very long exposure
of darkness.
2023-10-17 15:31:55 +02:00
e79574fd56 meson.build: update comments 2023-09-07 05:35:50 +02:00
93ad75eb35 Switch to a GAction-based menu
The new menu has a few more entries, and shows accelerators.

Most shortcuts have now moved from on_key_press() to actions,
and Alt-Shift-D has started working on macOS.

This also adds support for the global menu in macOS,
and moves some accelerators/key equivalents to the Command key.
There is no other easy way of accessing that global menu in GTK+.
2023-08-07 08:55:41 +02:00
2d10aa8b61 Prevent a class of crashes in monitoring 2023-08-03 04:42:50 +02:00
1ec41f7749 Remove inappropriate ellipses
The Information dialog doesn't need any user input.
2023-07-27 04:31:42 +02:00
d4b91d6260 Fix double colour management in the librsvg loader 2023-07-13 08:04:41 +02:00
5ec5f5bdbd Slightly optimize SVG loading 2023-07-09 10:40:32 +02:00
840e7f172c Colour-manage SVGs 2023-07-09 10:40:32 +02:00
9b99de99bb Fix crash in the librsvg loader 2023-07-09 04:39:35 +02:00
ab75d2b61d Fix build under Cygwin 2023-07-07 12:01:12 +02:00
92deba3890 Silence a compiler warning 2023-07-03 20:03:07 +02:00
668c5eb78a README.adoc: update package information 2023-07-01 21:30:20 +02:00
d713d5820c Fix installation within a Nix environment 2023-06-29 20:33:46 +02:00
f05e66bfc1 Fix compatibility with newer resvg versions 2023-06-29 03:36:34 +02:00
6ee5f69bfe Fix build within a Nix environment
Add a missing direct link dependency on libjpeg.
2023-06-27 22:48:48 +02:00
4249898497 Fix build without JPEG Quant Smooth 2023-06-27 22:40:29 +02:00
117422ade5 Fix build instructions, add .deb generation 2023-06-27 19:04:48 +02:00
8ff33e6b63 msys2-package.sh: fix iconv transliteration
LC_ALL overrides LC_CTYPE.

Even though C.UTF-8 may produce warnings, at least it works.
2023-06-27 00:36:00 +02:00
ce4a13ed38 msys2-install.sh: don't install the whole MIME DB 2023-06-27 00:36:00 +02:00
6a1b851130 Add libjxl to Windows packages
The library currently gets loaded through GdkPixbuf.
2023-06-26 21:38:59 +02:00
68245b55c9 msys2-configure: only extract what we need
In case the packages directory has been preloaded or symlinked.
2023-06-26 21:38:59 +02:00
2869c656c1 Centralize the project's URL 2023-06-26 15:46:10 +02:00
ec713b633e Package the MSI from within a custom target 2023-06-26 15:34:10 +02:00
88234f8283 Clean up the WiX XML a bit 2023-06-26 12:39:12 +02:00
49ee551b9b Use LocalAppData for thumbnails on Windows 2023-06-26 02:11:12 +02:00
089c90004b Produce a basic Windows installer package
We're very early adopters of msitools' new UI feature,
so this doesn't work on MSYS2 directly yet due to an old version.
2023-06-26 02:10:31 +02:00
31 changed files with 2019 additions and 655 deletions

View File

@@ -1,4 +1,4 @@
Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

View File

@@ -13,7 +13,7 @@ Features
photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
modules manage to load.
- Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
- Makes use of 30-bit X.org visuals, whenever it's possible and appropriate.
- Can make use of 30-bit X.org visuals, under certain conditions.
- Has a notion of pages, and tries to load all included content within files.
- Can keep the zoom and position when browsing, to help with comparing
zoomed-in images.
@@ -33,15 +33,20 @@ Not necessarily in this order.
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR.
Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version using Arch Linux's
https://aur.archlinux.org/packages/fiv-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
https://janouch.name/cd[Windows installers can be found here],
you want the _x86_64_ version.
Building and Running
--------------------
Build-only dependencies:
Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
libturbojpeg, libwebp, librsvg-2.0 (for icons) +
libturbojpeg, libwebp, libepoxy, librsvg-2.0 (for icons) +
Optional dependencies: lcms2, Little CMS fast float plugin,
LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
resvg (unstable API, needs to be requested explicitly) +
@@ -49,30 +54,34 @@ Runtime dependencies for reverse image search:
xdg-utils, cURL, jq
$ git clone --recursive https://git.janouch.name/p/fiv.git
$ cd fiv
$ meson setup builddir
$ cd builddir
$ meson compile
Considering the vast amount of dynamically-linked dependencies, do not attempt
direct installations via `ninja install`. To test the program:
$ meson devenv fiv
The lossless JPEG cropper and reverse image search are intended to be invoked
from a context menu.
from a file manager context menu.
For proper integration, you will need to install the application. On Debian,
you can get a quick and dirty installation package for testing purposes using:
$ meson compile deb
# dpkg -i fiv-*.deb
Windows
~~~~~~~
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
dependencies listed at the beginning of 'msys2-configure.sh',
plus rsvg-convert from librsvg2, and icotool from icoutils.
plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
Beware that the build will take up about a gigabyte of disk space.
$ sh -e msys2-configure.sh builddir
$ meson install -C builddir
$ meson compile package -C builddir
If everything succeeds, you will find a portable build of the application
in the 'builddir/package' subdirectory. No installer is provided yet.
in the 'builddir/package' subdirectory, and a very basic MSI installation
package in 'builddir'.
Faster colour management
^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -32,6 +32,10 @@ Options
handler to implement the "Open Containing Folder" feature of certain
applications.
*--collection*::
Always put arguments in a virtual directory, even when only one is passed.
Implies *--browse*.
*--help-all*::
Show the full list of options, including those provided by GTK+.

View File

@@ -16,7 +16,7 @@ q:lang(en):after { content: ""; }
<p class="details">
<span id="author">Přemysl Eric Janouch</span><br>
<span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br>
<span id="revnumber">version 0.0.0,</span>
<span id="revnumber">version 1.0.0,</span>
<span id="revdate">2023-04-17</span>
<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

@@ -1,7 +1,7 @@
//
// fiv-browser.c: filesystem browsing widget
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2025, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -17,9 +17,6 @@
#include "config.h"
#include <math.h>
#include <pixman.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@@ -27,6 +24,10 @@
#ifdef GDK_WINDOWING_QUARTZ
#include <gdk/gdkquartz.h>
#endif // GDK_WINDOWING_QUARTZ
#include <pixman.h>
#include <math.h>
#include <stdlib.h>
#include "fiv-browser.h"
#include "fiv-collection.h"
@@ -91,7 +92,8 @@ struct _FivBrowser {
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
size_t thumbnailers_len; ///< Thumbnailers array size
GQueue thumbnailers_queue; ///< Queued up Entry pointers
GQueue thumbnailers_queue_1; ///< Queued up Entry pointers, hi-prio
GQueue thumbnailers_queue_2; ///< Queued up Entry pointers, lo-prio
GdkCursor *pointer; ///< Cached pointer cursor
cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners
@@ -240,10 +242,12 @@ relayout(FivBrowser *self, int width)
pango_layout_set_wrap(label, PANGO_WRAP_WORD_CHAR);
pango_layout_set_ellipsize(label, PANGO_ELLIPSIZE_END);
#if PANGO_VERSION_CHECK(1, 44, 0)
PangoAttrList *attrs = pango_attr_list_new();
pango_attr_list_insert(attrs, pango_attr_insert_hyphens_new(FALSE));
pango_layout_set_attributes(label, attrs);
pango_attr_list_unref (attrs);
#endif
}
g_array_append_val(items, ((Item) {
@@ -272,14 +276,13 @@ relayout(FivBrowser *self, int width)
gtk_adjustment_set_page_size(self->hadjustment, width);
}
if (self->vadjustment) {
int height = gtk_widget_get_allocated_height(widget);
gtk_adjustment_set_lower(self->vadjustment, 0);
gtk_adjustment_set_upper(self->vadjustment, total_height);
gtk_adjustment_set_upper(self->vadjustment, MAX(height, total_height));
gtk_adjustment_set_step_increment(self->vadjustment,
self->item_height + self->item_spacing + 2 * self->item_border_y);
gtk_adjustment_set_page_increment(
self->vadjustment, gtk_widget_get_allocated_height(widget) * 0.9);
gtk_adjustment_set_page_size(
self->vadjustment, gtk_widget_get_allocated_height(widget));
gtk_adjustment_set_page_increment(self->vadjustment, height * 0.9);
gtk_adjustment_set_page_size(self->vadjustment, height);
}
return total_height;
}
@@ -740,7 +743,7 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
(void *) (intptr_t) 1, NULL);
g_queue_push_tail(&self->thumbnailers_queue, entry);
g_queue_push_tail(&self->thumbnailers_queue_2, entry);
}
entry_set_surface_user_data(entry);
@@ -794,13 +797,21 @@ on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
thumbnailer_next(t);
}
// TODO(p): Try to keep the minions alive (stdout will be a problem).
static gboolean
thumbnailer_next(Thumbnailer *t)
{
// TODO(p): Try to keep the minions alive (stdout will be a problem).
// Already have something to do, not a failure.
if (t->target)
return TRUE;
// They could have been removed via post-reload changes in the model.
FivBrowser *self = t->self;
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue)))
do {
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue_1)) &&
!(t->target = g_queue_pop_head(&self->thumbnailers_queue_2)))
return FALSE;
} while (t->target->removed);
// Case analysis:
// - We haven't found any thumbnail for the entry at all
@@ -817,9 +828,18 @@ thumbnailer_next(Thumbnailer *t)
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
"--", uri, NULL};
GSubprocessLauncher *launcher =
g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE);
#ifdef G_OS_WIN32
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
g_subprocess_launcher_set_cwd(launcher, prefix);
g_free(prefix);
#endif
GError *error = NULL;
t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower,
G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
t->minion = g_subprocess_launcher_spawnv(
launcher, t->target->icon ? argv_faster : argv_slower, &error);
g_object_unref(launcher);
if (error) {
g_warning("%s", error->message);
g_error_free(error);
@@ -837,7 +857,8 @@ thumbnailer_next(Thumbnailer *t)
static void
thumbnailers_abort(FivBrowser *self)
{
g_queue_clear(&self->thumbnailers_queue);
g_queue_clear(&self->thumbnailers_queue_1);
g_queue_clear(&self->thumbnailers_queue_2);
for (size_t i = 0; i < self->thumbnailers_len; i++) {
Thumbnailer *t = self->thumbnailers + i;
@@ -853,35 +874,35 @@ thumbnailers_abort(FivBrowser *self)
}
static void
thumbnailers_start(FivBrowser *self)
thumbnailers_enqueue(FivBrowser *self, Entry *entry)
{
thumbnailers_abort(self);
if (!self->model)
return;
GQueue lq = G_QUEUE_INIT;
for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = self->entries->pdata[i];
if (entry->removed)
continue;
if (!entry->removed) {
if (entry->icon)
g_queue_push_tail(&self->thumbnailers_queue, entry);
g_queue_push_tail(&self->thumbnailers_queue_1, entry);
else if (cairo_surface_get_user_data(
entry->thumbnail, &fiv_thumbnail_key_lq))
g_queue_push_tail(&lq, entry);
g_queue_push_tail(&self->thumbnailers_queue_2, entry);
}
while (!g_queue_is_empty(&lq)) {
g_queue_push_tail_link(
&self->thumbnailers_queue, g_queue_pop_head_link(&lq));
}
static void
thumbnailers_deploy(FivBrowser *self)
{
for (size_t i = 0; i < self->thumbnailers_len; i++) {
if (!thumbnailer_next(self->thumbnailers + i))
break;
}
}
static void
thumbnailers_restart(FivBrowser *self)
{
thumbnailers_abort(self);
for (guint i = 0; i < self->entries->len; i++)
thumbnailers_enqueue(self, self->entries->pdata[i]);
thumbnailers_deploy(self);
}
// --- Boilerplate -------------------------------------------------------------
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
@@ -1002,7 +1023,7 @@ set_item_size(FivBrowser *self, FivThumbnailSize size)
g_hash_table_remove_all(self->thumbnail_cache);
reload_thumbnails(self);
thumbnailers_start(self);
thumbnailers_restart(self);
g_object_notify_by_pspec(
G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
@@ -1302,10 +1323,14 @@ fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
if (!entry || entry != entry_at(self, event->x, event->y))
return GDK_EVENT_PROPAGATE;
GdkModifierType primary = gdk_keymap_get_modifier_mask(
gdk_keymap_get_for_display(gtk_widget_get_display(widget)),
GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
guint state = event->state & gtk_accelerator_get_default_mod_mask();
if ((event->button == GDK_BUTTON_PRIMARY && state == 0))
return open_entry(widget, entry, FALSE);
if ((event->button == GDK_BUTTON_PRIMARY && state == GDK_CONTROL_MASK) ||
if ((event->button == GDK_BUTTON_PRIMARY && state == primary) ||
(event->button == GDK_BUTTON_MIDDLE && state == 0))
return open_entry(widget, entry, TRUE);
return GDK_EVENT_PROPAGATE;
@@ -1560,6 +1585,14 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
case 0:
switch (event->keyval) {
case GDK_KEY_Delete:
if (self->selected) {
GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
GFile *file = g_file_new_for_uri(self->selected->e->uri);
fiv_context_menu_remove(window, file);
g_object_unref(file);
}
return GDK_EVENT_STOP;
case GDK_KEY_Return:
if (self->selected)
return open_entry(widget, self->selected, FALSE);
@@ -1862,7 +1895,8 @@ fiv_browser_init(FivBrowser *self)
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
for (size_t i = 0; i < self->thumbnailers_len; i++)
self->thumbnailers[i].self = self;
g_queue_init(&self->thumbnailers_queue);
g_queue_init(&self->thumbnailers_queue_1);
g_queue_init(&self->thumbnailers_queue_2);
set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL);
self->show_labels = FALSE;
@@ -1907,8 +1941,9 @@ on_model_reloaded(FivIoModel *model, FivBrowser *self)
fiv_browser_select(self, selected_uri);
g_free(selected_uri);
// Restarting thumbnailers is critical, because they keep Entry pointers.
reload_thumbnails(self);
thumbnailers_start(self);
thumbnailers_restart(self);
}
static void
@@ -1923,8 +1958,8 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
g_ptr_array_add(self->entries, entry);
reload_one_thumbnail(self, entry);
// TODO(p): Try to add to thumbnailer queue if already started.
thumbnailers_start(self);
thumbnailers_enqueue(self, entry);
thumbnailers_deploy(self);
return;
}
@@ -1950,8 +1985,9 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
// so that there's no jumping around. Or, a bit more properly,
// move the thumbnail cache entry to the new URI.
reload_one_thumbnail(self, found);
// TODO(p): Try to add to thumbnailer queue if already started.
thumbnailers_start(self);
// TODO(p): Rather cancel the entry in any running thumbnailer,
// remove it from queues, and _enqueue() + _deploy().
thumbnailers_restart(self);
} else {
found->removed = TRUE;
gtk_widget_queue_draw(GTK_WIDGET(self));

View File

@@ -528,12 +528,16 @@ fiv_collection_file_query_info(GFile *file, const char *attributes,
g_file_info_set_name(info, basename);
g_free(basename);
if ((name = g_file_info_get_display_name(info))) {
if (g_file_info_has_attribute(
info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) &&
(name = g_file_info_get_display_name(info))) {
gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_display_name(info, prefixed);
g_free(prefixed);
}
if ((name = g_file_info_get_edit_name(info))) {
if (g_file_info_has_attribute(
info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME) &&
(name = g_file_info_get_edit_name(info))) {
gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_edit_name(info, prefixed);
g_free(prefixed);

View File

@@ -1,7 +1,7 @@
//
// fiv-context-menu.c: popup menu
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -185,15 +185,24 @@ info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
if (bytes_in)
flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
GSubprocessLauncher *launcher = g_subprocess_launcher_new(flags);
#ifdef G_OS_WIN32
// Both to find wperl, and then to let wperl find the nearby exiftool.
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
g_subprocess_launcher_set_cwd(launcher, prefix);
g_free(prefix);
#endif
// TODO(p): Add a fallback to internal capabilities.
// The simplest is to specify the filename and the resolution.
GError *error = NULL;
GSubprocess *subprocess = g_subprocess_new(flags, &error,
GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
#ifdef G_OS_WIN32
"wperl",
#endif
"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
"--binary", "-quiet", "--", path, NULL);
g_object_unref(launcher);
if (error) {
info_redirect_error(dialog, error);
return;
@@ -328,17 +337,13 @@ open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
}
static void
open_context_show_error_dialog(OpenContext *self, GError *error)
show_error_dialog(GtkWindow *parent, GError *error)
{
GtkWindow *window = g_weak_ref_get(&self->window);
GtkWidget *dialog =
gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL,
gtk_message_dialog_new(GTK_WINDOW(parent), GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
g_clear_object(&window);
g_error_free(error);
}
@@ -357,7 +362,9 @@ open_context_launch(GtkWidget *widget, OpenContext *self)
(void) g_app_info_set_as_last_used_for_type(
self->app_info, self->content_type, NULL);
} else {
open_context_show_error_dialog(self, error);
GtkWindow *window = g_weak_ref_get(&self->window);
show_error_dialog(window, error);
g_clear_object(&window);
}
g_list_free(files);
g_object_unref(context);
@@ -437,14 +444,22 @@ on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
g_free(uri);
}
void
fiv_context_menu_remove(GtkWindow *parent, GFile *file)
{
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
GError *error = NULL;
if (!g_file_trash(file, NULL, &error))
show_error_dialog(parent, error);
}
static void
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
{
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
OpenContext *ctx = user_data;
GError *error = NULL;
if (!g_file_trash(ctx->file, NULL, &error))
open_context_show_error_dialog(ctx, error);
GtkWindow *window = g_weak_ref_get(&ctx->window);
fiv_context_menu_remove(window, ctx->file);
g_clear_object(&window);
}
static gboolean
@@ -541,7 +556,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
item = gtk_menu_item_new_with_mnemonic("_Information...");
item = gtk_menu_item_new_with_mnemonic("_Information");
g_signal_connect_data(item, "activate", G_CALLBACK(on_info_activate),
g_rc_box_acquire(ctx), open_context_unref, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);

View File

@@ -1,7 +1,7 @@
//
// fiv-context-menu.h: popup menu
//
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -18,4 +18,5 @@
#include <gtk/gtk.h>
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);

463
fiv-io-cmm.c Normal file
View File

@@ -0,0 +1,463 @@
//
// fiv-io-cmm.c: colour management
//
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include "config.h"
#include <glib.h>
#include <stdbool.h>
#include "fiv-io.h"
// Colour management must be handled before RGB conversions.
// TODO(p): Make it also possible to use Skia's skcms.
#ifdef HAVE_LCMS2
#include <lcms2.h>
#endif // HAVE_LCMS2
#ifdef HAVE_LCMS2_FAST_FLOAT
#include <lcms2_fast_float.h>
#endif // HAVE_LCMS2_FAST_FLOAT
// --- CMM-independent transforms ----------------------------------------------
// 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.
// It will typically produce horribly oversaturated results.
// 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;
}
}
// From libwebp, verified to exactly match [x * a / 255].
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
void
fiv_io_premultiply_argb32(FivIoImage *image)
{
if (image->format != CAIRO_FORMAT_ARGB32)
return;
for (uint32_t y = 0; y < image->height; y++) {
uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
for (uint32_t x = 0; x < image->width; x++) {
uint32_t argb = dstp[x], a = argb >> 24;
dstp[x] = a << 24 |
PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 |
PREMULTIPLY8(a, 0xFF & argb);
}
}
}
// --- Profiles ----------------------------------------------------------------
#ifdef HAVE_LCMS2
struct _FivIoProfile {
FivIoCmm *cmm;
cmsHPROFILE profile;
};
GBytes *
fiv_io_profile_to_bytes(FivIoProfile *profile)
{
cmsUInt32Number len = 0;
(void) cmsSaveProfileToMem(profile, NULL, &len);
gchar *data = g_malloc0(len);
if (!cmsSaveProfileToMem(profile, data, &len)) {
g_free(data);
return NULL;
}
return g_bytes_new_take(data, len);
}
static FivIoProfile *
fiv_io_profile_new(FivIoCmm *cmm, cmsHPROFILE profile)
{
FivIoProfile *self = g_new0(FivIoProfile, 1);
self->cmm = g_object_ref(cmm);
self->profile = profile;
return self;
}
void
fiv_io_profile_free(FivIoProfile *self)
{
cmsCloseProfile(self->profile);
g_clear_object(&self->cmm);
g_free(self);
}
#else // ! HAVE_LCMS2
GBytes *fiv_io_profile_to_bytes(FivIoProfile *) { return NULL; }
void fiv_io_profile_free(FivIoProfile *) {}
#endif // ! HAVE_LCMS2
// --- Contexts ----------------------------------------------------------------
#ifdef HAVE_LCMS2
struct _FivIoCmm {
GObject parent_instance;
cmsContext context;
// https://github.com/mm2/Little-CMS/issues/430
gboolean broken_premul;
};
G_DEFINE_TYPE(FivIoCmm, fiv_io_cmm, G_TYPE_OBJECT)
static void
fiv_io_cmm_finalize(GObject *gobject)
{
FivIoCmm *self = FIV_IO_CMM(gobject);
cmsDeleteContext(self->context);
G_OBJECT_CLASS(fiv_io_cmm_parent_class)->finalize(gobject);
}
static void
fiv_io_cmm_class_init(FivIoCmmClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fiv_io_cmm_finalize;
}
static void
fiv_io_cmm_init(FivIoCmm *self)
{
self->context = cmsCreateContext(NULL, self);
#ifdef HAVE_LCMS2_FAST_FLOAT
if (cmsPluginTHR(self->context, cmsFastFloatExtensions()))
self->broken_premul = LCMS_VERSION <= 2160;
#endif // HAVE_LCMS2_FAST_FLOAT
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FivIoCmm *
fiv_io_cmm_get_default(void)
{
static gsize initialization_value = 0;
static FivIoCmm *default_ = NULL;
if (g_once_init_enter(&initialization_value)) {
gsize setup_value = 1;
default_ = g_object_new(FIV_TYPE_IO_CMM, NULL);
g_once_init_leave(&initialization_value, setup_value);
}
return default_;
}
FivIoProfile *
fiv_io_cmm_get_profile(FivIoCmm *self, const void *data, size_t len)
{
g_return_val_if_fail(self != NULL, NULL);
return fiv_io_profile_new(self,
cmsOpenProfileFromMemTHR(self->context, data, len));
}
FivIoProfile *
fiv_io_cmm_get_profile_sRGB(FivIoCmm *self)
{
g_return_val_if_fail(self != NULL, NULL);
return fiv_io_profile_new(self,
cmsCreate_sRGBProfileTHR(self->context));
}
FivIoProfile *
fiv_io_cmm_get_profile_parametric(FivIoCmm *self,
double gamma, double whitepoint[2], double primaries[6])
{
g_return_val_if_fail(self != NULL, NULL);
const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0};
const cmsCIExyYTRIPLE cmsP = {
{primaries[0], primaries[1], 1.0},
{primaries[2], primaries[3], 1.0},
{primaries[4], primaries[5], 1.0},
};
cmsToneCurve *curve = cmsBuildGamma(self->context, gamma);
if (!curve)
return NULL;
cmsHPROFILE profile = cmsCreateRGBProfileTHR(self->context,
&cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
cmsFreeToneCurve(curve);
return fiv_io_profile_new(self, profile);
}
#else // ! HAVE_LCMS2
FivIoCmm *
fiv_io_cmm_get_default()
{
return NULL;
}
FivIoProfile *
fiv_io_cmm_get_profile(FivIoCmm *, const void *, size_t)
{
return NULL;
}
FivIoProfile *
fiv_io_cmm_get_profile_sRGB(FivIoCmm *)
{
return NULL;
}
FivIoProfile *
fiv_io_cmm_get_profile_parametric(FivIoCmm *, double, double[2], double[6])
{
return NULL;
}
#endif // ! HAVE_LCMS2
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FivIoProfile *
fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma)
{
return fiv_io_cmm_get_profile_parametric(self, gamma,
(double[2]){0.3127, 0.3290},
(double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
}
FivIoProfile *
fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes)
{
gsize len = 0;
gconstpointer p = g_bytes_get_data(bytes, &len);
return fiv_io_cmm_get_profile(self, p, len);
}
// --- Image loading -----------------------------------------------------------
#ifdef HAVE_LCMS2
// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
#define FIV_IO_PROFILE_ARGB32 \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
#define FIV_IO_PROFILE_4X16LE \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
void
fiv_io_cmm_cmyk(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
g_return_if_fail(target == NULL || self != NULL);
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransformTHR(self->context,
source->profile, TYPE_CMYK_8_REV,
target->profile, FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(
transform, image->data, image->data, image->width * image->height);
cmsDeleteTransform(transform);
return;
}
trivial_cmyk_to_host_byte_order_argb(
image->data, image->width * image->height);
}
static bool
fiv_io_cmm_rgb_direct(FivIoCmm *self, unsigned char *data, int w, int h,
FivIoProfile *source, FivIoProfile *target,
uint32_t source_format, uint32_t target_format)
{
g_return_val_if_fail(target == NULL || self != NULL, false);
// TODO(p): We should make this optional.
FivIoProfile *src_fallback = NULL;
if (target && !source)
source = src_fallback = fiv_io_cmm_get_profile_sRGB(self);
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransformTHR(self->context,
source->profile, source_format,
target->profile, target_format, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(transform, data, data, w * h);
cmsDeleteTransform(transform);
}
if (src_fallback)
fiv_io_profile_free(src_fallback);
return transform != NULL;
}
static void
fiv_io_cmm_xrgb32(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
}
void
fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
int w, int h, FivIoProfile *source, FivIoProfile *target)
{
fiv_io_cmm_rgb_direct(self, data, w, h, source, target,
FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
}
#else // ! HAVE_LCMS2
void
fiv_io_cmm_cmyk(FivIoCmm *, FivIoImage *image, FivIoProfile *, FivIoProfile *)
{
trivial_cmyk_to_host_byte_order_argb(
image->data, image->width * image->height);
}
static void
fiv_io_cmm_xrgb32(FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *)
{
}
void
fiv_io_cmm_4x16le_direct(
FivIoCmm *, unsigned char *, int, int, FivIoProfile *, FivIoProfile *)
{
}
#endif // ! HAVE_LCMS2
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
#define FIV_IO_PROFILE_ARGB32_PREMUL \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
static void
fiv_io_cmm_argb32(FivIoCmm *self, FivIoImage *image,
FivIoProfile *source, FivIoProfile *target)
{
g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
// TODO: With self->broken_premul,
// this probably also needs to be wrapped in un-premultiplication.
fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
source, target,
FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
}
void
fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
g_return_if_fail(target == NULL || self != NULL);
if (image->format != CAIRO_FORMAT_ARGB32) {
fiv_io_cmm_xrgb32(self, image, source, target);
} else if (!target || self->broken_premul) {
fiv_io_cmm_xrgb32(self, image, source, target);
fiv_io_premultiply_argb32(image);
} else if (!fiv_io_cmm_rgb_direct(self, image->data,
image->width, image->height, source, target,
FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
g_debug("failed to create a premultiplying transform");
fiv_io_premultiply_argb32(image);
}
}
#else // ! HAVE_LCMS2 || LCMS_VERSION < 2130
static void
fiv_io_cmm_argb32(G_GNUC_UNUSED FivIoCmm *self, G_GNUC_UNUSED FivIoImage *image,
G_GNUC_UNUSED FivIoProfile *source, G_GNUC_UNUSED FivIoProfile *target)
{
// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
}
void
fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
fiv_io_cmm_xrgb32(self, image, source, target);
fiv_io_premultiply_argb32(image);
}
#endif // ! HAVE_LCMS2 || LCMS_VERSION < 2130
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void
fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
void (*frame_cb) (FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *))
{
FivIoProfile *source = NULL;
if (page->icc)
source = fiv_io_cmm_get_profile_from_bytes(self, page->icc);
// TODO(p): All animations need to be composited in a linear colour space.
for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
frame_cb(self, frame, source, target);
if (source)
fiv_io_profile_free(source);
}
void
fiv_io_cmm_any(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
// TODO(p): Ensure we do colour management early enough, so that
// no avoidable increase of quantization error occurs beforehands,
// and also for correct alpha compositing.
switch (image->format) {
break; case CAIRO_FORMAT_RGB24:
fiv_io_cmm_xrgb32(self, image, source, target);
break; case CAIRO_FORMAT_ARGB32:
fiv_io_cmm_argb32(self, image, source, target);
break; default:
g_debug("CM attempted on an unsupported surface format");
}
}
// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
FivIoImage *
fiv_io_cmm_finish(FivIoCmm *self, FivIoImage *image, FivIoProfile *target)
{
if (!target)
return image;
for (FivIoImage *page = image; page != NULL; page = page->page_next)
fiv_io_cmm_page(self, page, target, fiv_io_cmm_any);
return image;
}

View File

@@ -247,7 +247,9 @@ static GPtrArray *
model_decide_placement(
FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
{
if (self->filtering && g_file_info_get_is_hidden(info))
if (self->filtering &&
g_file_info_has_attribute(info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
g_file_info_get_is_hidden(info))
return NULL;
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
return subdirs;
@@ -346,6 +348,8 @@ static void
monitor_apply(enum monitor_event event, GPtrArray *target, int index,
FivIoModelEntry *new_entry)
{
g_return_if_fail(event != MONITOR_CHANGING || index >= 0);
if (event == MONITOR_RENAMING && index < 0)
// The file used to be filtered out but isn't anymore.
event = MONITOR_ADDING;
@@ -378,6 +382,9 @@ on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
switch (event_type) {
case G_FILE_MONITOR_EVENT_CHANGED:
case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
// On macOS, we seem to not receive _CHANGED for child files.
// And while this seems to arrive too early, it's a mild improvement.
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
event = MONITOR_CHANGING;
new_entry_file = file;
break;
@@ -396,8 +403,6 @@ on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
new_entry_file = file;
break;
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
// TODO(p): Figure out if we can't make use of _CHANGES_DONE_HINT.
case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
case G_FILE_MONITOR_EVENT_UNMOUNTED:
// TODO(p): Figure out how to handle _UNMOUNTED sensibly.

561
fiv-io.c
View File

@@ -1,7 +1,7 @@
//
// fiv-io.c: image operations
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -34,11 +34,6 @@
#include <libjpegqs.h>
#endif // HAVE_JPEG_QS
// Colour management must be handled before RGB conversions.
#ifdef HAVE_LCMS2
#include <lcms2.h>
#endif // HAVE_LCMS2
#define TIFF_TABLES_CONSTANTS_ONLY
#include "tiff-tables.h"
#include "tiffer.h"
@@ -47,6 +42,8 @@
#include <libraw.h>
#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0
#else
#define rawparams params
#endif
#endif // HAVE_LIBRAW
#ifdef HAVE_RESVG
@@ -64,6 +61,9 @@
#ifdef HAVE_LIBTIFF
#include <tiff.h>
#include <tiffio.h>
#ifndef TIFF_TMSIZE_T_MAX
#define TIFF_TMSIZE_T_MAX ((tmsize_t) (SIZE_MAX >> 1))
#endif
#endif // HAVE_LIBTIFF
#ifdef HAVE_GDKPIXBUF
#include <gdk-pixbuf/gdk-pixbuf.h>
@@ -291,318 +291,6 @@ try_append_page(
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
}
FivIoProfile
fiv_io_profile_new_sRGB_gamma(double gamma)
{
#ifdef HAVE_LCMS2
// TODO(p): Make sure to use the library in a thread-safe manner.
cmsContext context = NULL;
static const cmsCIExyY D65 = {0.3127, 0.3290, 1.0};
static const cmsCIExyYTRIPLE primaries = {
{0.6400, 0.3300, 1.0}, {0.3000, 0.6000, 1.0}, {0.1500, 0.0600, 1.0}};
cmsToneCurve *curve = cmsBuildGamma(context, gamma);
if (!curve)
return NULL;
cmsHPROFILE profile = cmsCreateRGBProfileTHR(
context, &D65, &primaries, (cmsToneCurve *[3]){curve, curve, curve});
cmsFreeToneCurve(curve);
return profile;
#else
(void) gamma;
return NULL;
#endif
}
static FivIoProfile
fiv_io_profile_new_from_bytes(GBytes *bytes)
{
gsize len = 0;
gconstpointer p = g_bytes_get_data(bytes, &len);
return fiv_io_profile_new(p, len);
}
static GBytes *
fiv_io_profile_to_bytes(FivIoProfile profile)
{
#ifdef HAVE_LCMS2
cmsUInt32Number len = 0;
(void) cmsSaveProfileToMem(profile, NULL, &len);
gchar *data = g_malloc0(len);
if (!cmsSaveProfileToMem(profile, data, &len)) {
g_free(data);
return NULL;
}
return g_bytes_new_take(data, len);
#else
(void) profile;
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.
#ifndef HAVE_LCMS2
#define FIV_IO_PROFILE_ARGB32 0
#define FIV_IO_PROFILE_4X16LE 0
#else
#define FIV_IO_PROFILE_ARGB32 \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
#define FIV_IO_PROFILE_4X16LE \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
#endif
// 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.
// It will typically produce horribly oversaturated results.
// 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(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
#ifndef HAVE_LCMS2
(void) source;
(void) target;
#else
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransform(source, TYPE_CMYK_8_REV, target,
FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(
transform, image->data, image->data, image->width * image->height);
cmsDeleteTransform(transform);
return;
}
#endif
trivial_cmyk_to_host_byte_order_argb(
image->data, image->width * image->height);
}
static bool
fiv_io_profile_rgb_direct(unsigned char *data, int w, int h,
FivIoProfile source, FivIoProfile target,
uint32_t source_format, uint32_t target_format)
{
#ifndef HAVE_LCMS2
(void) data;
(void) w;
(void) h;
(void) source;
(void) source_format;
(void) target;
(void) target_format;
return false;
#else
// TODO(p): We should make this optional.
cmsHPROFILE src_fallback = NULL;
if (target && !source)
source = src_fallback = cmsCreate_sRGBProfile();
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransform(
source, source_format, target, target_format, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(transform, data, data, w * h);
cmsDeleteTransform(transform);
}
if (src_fallback)
cmsCloseProfile(src_fallback);
return transform != NULL;
#endif
}
static void
fiv_io_profile_xrgb32(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
}
static void
fiv_io_profile_4x16le_direct(
unsigned char *data, int w, int h, FivIoProfile source, FivIoProfile target)
{
fiv_io_profile_rgb_direct(data, w, h, source, target,
FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_profile_page(FivIoImage *page, FivIoProfile target,
void (*frame_cb) (FivIoImage *, FivIoProfile, FivIoProfile))
{
FivIoProfile source = NULL;
if (page->icc)
source = fiv_io_profile_new_from_bytes(page->icc);
// TODO(p): All animations need to be composited in a linear colour space.
for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
frame_cb(frame, source, target);
if (source)
fiv_io_profile_free(source);
}
static void
fiv_io_premultiply_argb32(FivIoImage *image)
{
if (image->format != CAIRO_FORMAT_ARGB32)
return;
for (uint32_t y = 0; y < image->height; y++) {
uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
for (uint32_t x = 0; x < image->width; x++) {
uint32_t argb = dstp[x], a = argb >> 24;
dstp[x] = a << 24 |
PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 |
PREMULTIPLY8(a, 0xFF & argb);
}
}
}
#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
#define FIV_IO_PROFILE_ARGB32_PREMUL \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
static void
fiv_io_profile_argb32(FivIoImage *image,
FivIoProfile source, FivIoProfile target)
{
g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
source, target,
FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
}
static void
fiv_io_profile_argb32_premultiply(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
if (image->format != CAIRO_FORMAT_ARGB32) {
fiv_io_profile_xrgb32(image, source, target);
} else if (!fiv_io_profile_rgb_direct(image->data,
image->width, image->height, source, target,
FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
g_debug("failed to create a premultiplying transform");
fiv_io_premultiply_argb32(image);
}
}
#else // ! HAVE_LCMS2 || LCMS_VERSION < 2130
// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
#define fiv_io_profile_argb32(surface, source, target)
static void
fiv_io_profile_argb32_premultiply(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
fiv_io_profile_xrgb32(image, source, target);
fiv_io_premultiply_argb32(image);
}
#endif // ! HAVE_LCMS2 || LCMS_VERSION < 2130
#define fiv_io_profile_argb32_premultiply_page(page, target) \
fiv_io_profile_page((page), (target), fiv_io_profile_argb32_premultiply)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_profile_any(FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
// TODO(p): Ensure we do colour management early enough, so that
// no avoidable increase of quantization error occurs beforehands,
// and also for correct alpha compositing.
switch (image->format) {
break; case CAIRO_FORMAT_RGB24:
fiv_io_profile_xrgb32(image, source, target);
break; case CAIRO_FORMAT_ARGB32:
fiv_io_profile_argb32(image, source, target);
break; default:
g_debug("CM attempted on an unsupported surface format");
}
}
// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
static FivIoImage *
fiv_io_profile_finalize(FivIoImage *image, FivIoProfile target)
{
if (!target)
return image;
for (FivIoImage *page = image; page != NULL; page = page->page_next)
fiv_io_profile_page(page, target, fiv_io_profile_any);
return image;
}
// --- Wuffs -------------------------------------------------------------------
static bool
@@ -696,8 +384,9 @@ struct load_wuffs_frame_context {
GBytes *meta_iccp; ///< Reference-counted ICC profile
GBytes *meta_xmp; ///< Reference-counted XMP
FivIoProfile target; ///< Target device profile, if any
FivIoProfile source; ///< Source colour profile, if any
FivIoCmm *cmm; ///< CMM context, if any
FivIoProfile *target; ///< Target device profile, if any
FivIoProfile *source; ///< Source colour profile, if any
FivIoImage *result; ///< The resulting image (referenced)
FivIoImage *result_tail; ///< The final animation frame
@@ -764,11 +453,12 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
if (ctx->target) {
if (ctx->expand_16_float || ctx->pack_16_10) {
fiv_io_profile_4x16le_direct(
fiv_io_cmm_4x16le_direct(ctx->cmm,
targetbuf, ctx->width, ctx->height, ctx->source, ctx->target);
// The first one premultiplies below, the second doesn't need to.
} else {
fiv_io_profile_argb32_premultiply(image, ctx->source, ctx->target);
fiv_io_cmm_argb32_premultiply(
ctx->cmm, image, ctx->source, ctx->target);
}
}
@@ -906,7 +596,8 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
const FivIoOpenContext *ioctx, GError **error)
{
struct load_wuffs_frame_context ctx = {
.dec = dec, .src = &src, .target = ioctx->screen_profile};
.dec = dec, .src = &src,
.cmm = ioctx->cmm, .target = ioctx->screen_profile};
// TODO(p): PNG text chunks, like we do with PNG thumbnails.
// TODO(p): See if something could and should be done about
@@ -990,9 +681,11 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
// TODO(p): Improve our simplistic PNG handling of: gAMA, cHRM, sRGB.
if (ctx.target) {
if (ctx.meta_iccp)
ctx.source = fiv_io_profile_new_from_bytes(ctx.meta_iccp);
ctx.source = fiv_io_cmm_get_profile_from_bytes(
ctx.cmm, ctx.meta_iccp);
else if (isfinite(gamma) && gamma > 0)
ctx.source = fiv_io_profile_new_sRGB_gamma(gamma);
ctx.source = fiv_io_cmm_get_profile_sRGB_gamma(
ctx.cmm, gamma);
}
// Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine.
@@ -1283,7 +976,7 @@ static uint32_t *
parse_mpf_index_entries(const struct tiffer *T, struct tiffer_entry *entry)
{
uint32_t count = entry->remaining_count / 16;
uint32_t *offsets = g_malloc0_n(sizeof *offsets, count + 1), *out = offsets;
uint32_t *offsets = g_malloc0_n(count + 1, sizeof *offsets), *out = offsets;
for (uint32_t i = 0; i < count; i++) {
// 5.2.3.3.3. Individual Image Data Offset
uint32_t offset = parse_mpf_mpentry(entry->p + i * 16, T);
@@ -1329,6 +1022,100 @@ parse_mpf(
// --- JPEG --------------------------------------------------------------------
struct exif_profile {
double whitepoint[2]; ///< TIFF_WhitePoint
double primaries[6]; ///< TIFF_PrimaryChromaticities
enum Exif_ColorSpace colorspace; ///< Exif_ColorSpace
double gamma; ///< Exif_Gamma
bool have_whitepoint;
bool have_primaries;
bool have_colorspace;
bool have_gamma;
};
static bool
parse_exif_profile_reals(
const struct tiffer *T, struct tiffer_entry *entry, double *out)
{
while (tiffer_real(T, entry, out++))
if (!tiffer_next_value(entry))
return false;
return true;
}
static void
parse_exif_profile_subifd(
struct exif_profile *params, const struct tiffer *T, uint32_t offset)
{
struct tiffer subT = {};
if (!tiffer_subifd(T, offset, &subT))
return;
struct tiffer_entry entry = {};
while (tiffer_next_entry(&subT, &entry)) {
int64_t value = 0;
if (G_UNLIKELY(entry.tag == Exif_ColorSpace) &&
entry.type == TIFFER_SHORT && entry.remaining_count == 1 &&
tiffer_integer(&subT, &entry, &value)) {
params->have_colorspace = true;
params->colorspace = value;
} else if (G_UNLIKELY(entry.tag == Exif_Gamma) &&
entry.type == TIFFER_RATIONAL && entry.remaining_count == 1 &&
tiffer_real(&subT, &entry, &params->gamma)) {
params->have_gamma = true;
}
}
}
static FivIoProfile *
parse_exif_profile(FivIoCmm *cmm, const void *data, size_t len)
{
struct tiffer T = {};
if (!tiffer_init(&T, (const uint8_t *) data, len) || !tiffer_next_ifd(&T))
return NULL;
struct exif_profile params = {};
struct tiffer_entry entry = {};
while (tiffer_next_entry(&T, &entry)) {
int64_t offset = 0;
if (G_UNLIKELY(entry.tag == TIFF_ExifIFDPointer) &&
entry.type == TIFFER_LONG && entry.remaining_count == 1 &&
tiffer_integer(&T, &entry, &offset) &&
offset >= 0 && offset <= UINT32_MAX) {
parse_exif_profile_subifd(&params, &T, offset);
} else if (G_UNLIKELY(entry.tag == TIFF_WhitePoint) &&
entry.type == TIFFER_RATIONAL &&
entry.remaining_count == G_N_ELEMENTS(params.whitepoint)) {
params.have_whitepoint =
parse_exif_profile_reals(&T, &entry, params.whitepoint);
} else if (G_UNLIKELY(entry.tag == TIFF_PrimaryChromaticities) &&
entry.type == TIFFER_RATIONAL &&
entry.remaining_count == G_N_ELEMENTS(params.primaries)) {
params.have_primaries =
parse_exif_profile_reals(&T, &entry, params.primaries);
}
}
if (!params.have_colorspace)
return NULL;
// If sRGB is claimed, assume all parameters are standard.
if (params.colorspace == Exif_ColorSpace_sRGB)
return fiv_io_cmm_get_profile_sRGB(cmm);
// AdobeRGB Nikon JPEGs provide all of these.
if (params.colorspace != Exif_ColorSpace_Uncalibrated ||
!params.have_gamma ||
!params.have_whitepoint ||
!params.have_primaries)
return NULL;
return fiv_io_cmm_get_profile_parametric(cmm,
params.gamma, params.whitepoint, params.primaries);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct jpeg_metadata {
GByteArray *exif; ///< Exif buffer or NULL
GByteArray *icc; ///< ICC profile buffer or NULL
@@ -1477,15 +1264,18 @@ load_jpeg_finalize(FivIoImage *image, bool cmyk,
else
g_byte_array_free(meta.icc, TRUE);
FivIoProfile source = NULL;
if (icc_profile)
source = fiv_io_profile_new(
FivIoProfile *source = NULL;
if (icc_profile && ctx->cmm)
source = fiv_io_cmm_get_profile(ctx->cmm,
g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile));
else if (image->exif && ctx->cmm)
source = parse_exif_profile(ctx->cmm,
g_bytes_get_data(image->exif, NULL), g_bytes_get_size(image->exif));
if (cmyk)
fiv_io_profile_cmyk(image, source, ctx->screen_profile);
fiv_io_cmm_cmyk(ctx->cmm, image, source, ctx->screen_profile);
else
fiv_io_profile_any(image, source, ctx->screen_profile);
fiv_io_cmm_any(ctx->cmm, image, source, ctx->screen_profile);
if (source)
fiv_io_profile_free(source);
@@ -1538,6 +1328,8 @@ load_libjpeg_turbo(const char *data, gsize len, const FivIoOpenContext *ctx,
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, (const unsigned char *) data, len);
(void) jpeg_read_header(&cinfo, true);
// TODO(p): With newer libjpeg-turbo, if cinfo.data_precision is 12 or 16,
// try to load it with higher precision.
bool use_cmyk = cinfo.jpeg_color_space == JCS_CMYK ||
cinfo.jpeg_color_space == JCS_YCCK;
@@ -1614,11 +1406,16 @@ load_libjpeg_enhanced(
{
// Go for the maximum quality setting.
jpegqs_control_t opts = {
.flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV | JPEGQS_UPSAMPLE_UV,
.flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV,
.threads = g_get_num_processors(),
.niter = 3,
};
// Waiting for https://github.com/ilyakurdyukov/jpeg-quantsmooth/issues/28
#if LIBJPEG_TURBO_VERSION_NUMBER < 2001090
opts.flags |= JPEGQS_UPSAMPLE_UV;
#endif
(void) jpegqs_start_decompress(cinfo, &opts);
while (cinfo->output_scanline < cinfo->output_height)
(void) jpeg_read_scanlines(cinfo, lines + cinfo->output_scanline,
@@ -1627,7 +1424,7 @@ load_libjpeg_enhanced(
}
#else
#define load_libjpeg_enhanced libjpeg_turbo_load_simple
#define load_libjpeg_enhanced load_libjpeg_simple
#endif
static FivIoImage *
@@ -1734,10 +1531,8 @@ load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
return NULL;
}
bool is_opaque = (info->bgcolor & 0xFF) == 0xFF;
uint64_t area = info->canvas_width * info->canvas_height;
FivIoImage *image = fiv_io_image_new(
is_opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32,
FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_RGB24,
info->canvas_width, info->canvas_height);
if (!image) {
set_error(error, "image allocation failure");
@@ -1748,9 +1543,18 @@ load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
if (G_BYTE_ORDER == G_LITTLE_ENDIAN) {
memcpy(dst, buf, area * sizeof *dst);
} else {
uint32_t *src = (uint32_t *) buf;
for (uint64_t i = 0; i < area; i++)
*dst++ = GUINT32_FROM_LE(*src++);
const uint32_t *src = (const uint32_t *) buf;
for (uint64_t i = 0; i < area; i++) {
uint32_t value = *src++;
*dst++ = GUINT32_FROM_LE(value);
}
}
// info->bgcolor is not reliable.
for (const uint32_t *p = dst, *end = dst + area; p < end; p++)
if ((~*p & 0xff000000)) {
image->format = CAIRO_FORMAT_ARGB32;
break;
}
// This API is confusing and awkward.
@@ -1874,7 +1678,8 @@ open_libwebp(
WebPDemuxDelete(demux);
if (ctx->screen_profile)
fiv_io_profile_argb32_premultiply_page(result, ctx->screen_profile);
fiv_io_cmm_argb32_premultiply_page(
ctx->cmm, result, ctx->screen_profile);
fail:
WebPFreeDecBuffer(&config.output);
@@ -2111,6 +1916,9 @@ load_tiff_ep(
orientation >= 1 && orientation <= 8) {
image->orientation = orientation;
}
// XXX: AdobeRGB Nikon NEFs can only be distinguished by a ColorSpace tag
// from within their MakerNote.
return image;
}
@@ -2253,7 +2061,7 @@ open_libraw(
out:
libraw_close(iprc);
return fiv_io_profile_finalize(result, ctx->screen_profile);
return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile);
}
#endif // HAVE_LIBRAW ---------------------------------------------------------
@@ -2275,8 +2083,8 @@ load_resvg_destroy(FivIoRenderClosure *closure)
}
static FivIoImage *
load_resvg_render_internal(FivIoRenderClosureResvg *self,
double scale, FivIoProfile target, GError **error)
load_resvg_render_internal(FivIoRenderClosureResvg *self, double scale,
FivIoCmm *cmm, FivIoProfile *target, GError **error)
{
double w = ceil(self->width * scale), h = ceil(self->height * scale);
if (w > SHRT_MAX || h > SHRT_MAX) {
@@ -2291,25 +2099,30 @@ load_resvg_render_internal(FivIoRenderClosureResvg *self,
}
uint32_t *pixels = (uint32_t *) image->data;
#if RESVG_MAJOR_VERSION == 0 && RESVG_MINOR_VERSION < 33
resvg_fit_to fit_to = {
scale == 1 ? RESVG_FIT_TO_TYPE_ORIGINAL : RESVG_FIT_TO_TYPE_ZOOM,
scale};
resvg_render(self->tree, fit_to, resvg_transform_identity(),
image->width, image->height, (char *) pixels);
#else
resvg_render(self->tree, (resvg_transform) {.a = scale, .d = scale},
image->width, image->height, (char *) pixels);
#endif
for (int i = 0; i < w * h; i++) {
uint32_t rgba = g_ntohl(pixels[i]);
pixels[i] = rgba << 24 | rgba >> 8;
}
return fiv_io_profile_finalize(image, target);
return fiv_io_cmm_finish(cmm, image, target);
}
static FivIoImage *
load_resvg_render(FivIoRenderClosure *closure, double scale)
load_resvg_render(FivIoRenderClosure *closure,
FivIoCmm *cmm, FivIoProfile *target, double scale)
{
FivIoRenderClosureResvg *self = (FivIoRenderClosureResvg *) closure;
// TODO(p): Somehow get the target colour management profile.
return load_resvg_render_internal(self, scale, NULL, NULL);
return load_resvg_render_internal(self, scale, cmm, target, NULL);
}
static const char *
@@ -2367,8 +2180,8 @@ open_resvg(
closure->width = size.width;
closure->height = size.height;
FivIoImage *image =
load_resvg_render_internal(closure, 1., ctx->screen_profile, error);
FivIoImage *image = load_resvg_render_internal(
closure, 1., ctx->cmm, ctx->screen_profile, error);
if (!image) {
load_resvg_destroy(&closure->parent);
return NULL;
@@ -2398,7 +2211,7 @@ load_librsvg_destroy(FivIoRenderClosure *closure)
static FivIoImage *
load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
FivIoProfile target, GError **error)
FivIoCmm *cmm, FivIoProfile *target, GError **error)
{
RsvgRectangle viewport = {.x = 0, .y = 0,
.width = self->width * scale, .height = self->height * scale};
@@ -2412,10 +2225,11 @@ load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
cairo_surface_t *surface = fiv_io_image_to_surface_noref(image);
cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
(void) rsvg_handle_render_document(self->handle, cr, &viewport, error);
gboolean success =
rsvg_handle_render_document(self->handle, cr, &viewport, error);
cairo_status_t status = cairo_status(cr);
cairo_destroy(cr);
if (error) {
if (!success) {
fiv_io_image_unref(image);
return NULL;
}
@@ -2424,15 +2238,15 @@ load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
fiv_io_image_unref(image);
return NULL;
}
return fiv_io_profile_finalize(image, target);
return fiv_io_cmm_finish(cmm, image, target);
}
static FivIoImage *
load_librsvg_render(FivIoRenderClosure *closure, double scale)
load_librsvg_render(FivIoRenderClosure *closure,
FivIoCmm *cmm, FivIoProfile *target, double scale)
{
FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure;
// TODO(p): Somehow get the target colour management profile.
return load_librsvg_render_internal(self, scale, NULL, NULL);
return load_librsvg_render_internal(self, scale, cmm, target, NULL);
}
static FivIoImage *
@@ -2481,15 +2295,15 @@ open_librsvg(
// librsvg rasterizes filters, so rendering to a recording Cairo surface
// has been abandoned.
FivIoImage *image =
load_librsvg_render_internal(closure, 1., ctx->screen_profile, error);
FivIoImage *image = load_librsvg_render_internal(
closure, 1., ctx->cmm, ctx->screen_profile, error);
if (!image) {
load_librsvg_destroy(&closure->parent);
return NULL;
}
image->render = &closure->parent;
return fiv_io_profile_finalize(image, ctx->screen_profile);
return image;
}
#endif // HAVE_LIBRSVG --------------------------------------------------------
@@ -2798,7 +2612,7 @@ open_libheif(
g_free(ids);
fail_read:
heif_context_free(ctx);
return fiv_io_profile_finalize(result, ioctx->screen_profile);
return fiv_io_cmm_finish(ioctx->cmm, result, ioctx->screen_profile);
}
#endif // HAVE_LIBHEIF --------------------------------------------------------
@@ -3029,7 +2843,7 @@ fail:
TIFFSetWarningHandlerExt(whe);
TIFFSetErrorHandler(eh);
TIFFSetWarningHandler(wh);
return fiv_io_profile_finalize(result, ctx->screen_profile);
return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile);
}
#endif // HAVE_LIBTIFF --------------------------------------------------------
@@ -3124,9 +2938,10 @@ open_gdkpixbuf(
g_object_unref(pixbuf);
if (custom_argb32)
fiv_io_profile_argb32_premultiply_page(image, ctx->screen_profile);
fiv_io_cmm_argb32_premultiply_page(
ctx->cmm, image, ctx->screen_profile);
else
image = fiv_io_profile_finalize(image, ctx->screen_profile);
image = fiv_io_cmm_finish(ctx->cmm, image, ctx->screen_profile);
return image;
}
@@ -3579,7 +3394,7 @@ set_metadata(WebPMux *mux, const char *fourcc, GBytes *data)
}
gboolean
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile target,
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile *target,
const char *path, GError **error)
{
g_return_val_if_fail(page != NULL, FALSE);
@@ -3643,34 +3458,40 @@ fiv_io_orientation_apply(const FivIoImage *image,
FivIoOrientation orientation, double *width, double *height)
{
fiv_io_orientation_dimensions(image, orientation, width, height);
return fiv_io_orientation_matrix(orientation, *width, *height);
}
cairo_matrix_t
fiv_io_orientation_matrix(
FivIoOrientation orientation, double width, double height)
{
cairo_matrix_t matrix = {};
cairo_matrix_init_identity(&matrix);
switch (orientation) {
case FivIoOrientation90:
cairo_matrix_rotate(&matrix, -M_PI_2);
cairo_matrix_translate(&matrix, -*width, 0);
cairo_matrix_translate(&matrix, -width, 0);
break;
case FivIoOrientation180:
cairo_matrix_scale(&matrix, -1, -1);
cairo_matrix_translate(&matrix, -*width, -*height);
cairo_matrix_translate(&matrix, -width, -height);
break;
case FivIoOrientation270:
cairo_matrix_rotate(&matrix, +M_PI_2);
cairo_matrix_translate(&matrix, 0, -*height);
cairo_matrix_translate(&matrix, 0, -height);
break;
case FivIoOrientationMirror0:
cairo_matrix_scale(&matrix, -1, +1);
cairo_matrix_translate(&matrix, -*width, 0);
cairo_matrix_translate(&matrix, -width, 0);
break;
case FivIoOrientationMirror90:
cairo_matrix_rotate(&matrix, +M_PI_2);
cairo_matrix_scale(&matrix, -1, +1);
cairo_matrix_translate(&matrix, -*width, -*height);
cairo_matrix_translate(&matrix, -width, -height);
break;
case FivIoOrientationMirror180:
cairo_matrix_scale(&matrix, +1, -1);
cairo_matrix_translate(&matrix, 0, -*height);
cairo_matrix_translate(&matrix, 0, -height);
break;
case FivIoOrientationMirror270:
cairo_matrix_rotate(&matrix, -M_PI_2);

View File

@@ -1,7 +1,7 @@
//
// fiv-io.h: image operations
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -22,16 +22,53 @@
#include <glib.h>
#include <webp/encode.h> // WebPConfig
typedef enum _FivIoOrientation FivIoOrientation;
typedef struct _FivIoRenderClosure FivIoRenderClosure;
typedef struct _FivIoImage FivIoImage;
typedef struct _FivIoProfile FivIoProfile;
// --- Colour management -------------------------------------------------------
// Note that without a CMM, all FivIoCmm and FivIoProfile will be returned NULL.
// TODO(p): Make it also possible to use Skia's skcms.
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);
GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
void fiv_io_profile_free(FivIoProfile *self);
// From libwebp, verified to exactly match [x * a / 255].
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define FIV_TYPE_IO_CMM (fiv_io_cmm_get_type())
G_DECLARE_FINAL_TYPE(FivIoCmm, fiv_io_cmm, FIV, IO_CMM, GObject)
FivIoCmm *fiv_io_cmm_get_default(void);
FivIoProfile *fiv_io_cmm_get_profile(
FivIoCmm *self, const void *data, size_t len);
FivIoProfile *fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes);
FivIoProfile *fiv_io_cmm_get_profile_sRGB(FivIoCmm *self);
FivIoProfile *fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma);
FivIoProfile *fiv_io_cmm_get_profile_parametric(
FivIoCmm *self, double gamma, double whitepoint[2], double primaries[6]);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void fiv_io_premultiply_argb32(FivIoImage *image);
void fiv_io_cmm_cmyk(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
void fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
int w, int h, FivIoProfile *source, FivIoProfile *target);
void fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
#define fiv_io_cmm_argb32_premultiply_page(cmm, page, target) \
fiv_io_cmm_page((cmm), (page), (target), fiv_io_cmm_argb32_premultiply)
void fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
void (*frame_cb) (FivIoCmm *,
FivIoImage *, FivIoProfile *, FivIoProfile *));
void fiv_io_cmm_any(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
FivIoImage *fiv_io_cmm_finish(FivIoCmm *self,
FivIoImage *image, FivIoProfile *target);
// --- Loading -----------------------------------------------------------------
@@ -39,10 +76,6 @@ extern const char *fiv_io_supported_media_types[];
gchar **fiv_io_all_supported_media_types(void);
typedef enum _FivIoOrientation FivIoOrientation;
typedef struct _FivIoRenderClosure FivIoRenderClosure;
typedef struct _FivIoImage FivIoImage;
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
enum _FivIoOrientation {
FivIoOrientationUnknown = 0,
@@ -56,9 +89,12 @@ enum _FivIoOrientation {
FivIoOrientation270 = 8
};
// TODO(p): Maybe make FivIoProfile a referencable type,
// then loaders could store it in their closures.
struct _FivIoRenderClosure {
/// The rendering is allowed to fail, returning NULL.
FivIoImage *(*render)(FivIoRenderClosure *, double scale);
FivIoImage *(*render)(
FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
void (*destroy)(FivIoRenderClosure *);
};
@@ -129,7 +165,8 @@ cairo_surface_t *fiv_io_image_to_surface_noref(const FivIoImage *image);
typedef struct {
const char *uri; ///< Source URI
FivIoProfile screen_profile; ///< Target colour space or NULL
FivIoCmm *cmm; ///< Colour management module or NULL
FivIoProfile *screen_profile; ///< Target colour space or NULL
int screen_dpi; ///< Target DPI
gboolean enhance; ///< Enhance JPEG (currently)
gboolean first_frame_only; ///< Only interested in the 1st frame
@@ -148,6 +185,8 @@ FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error);
/// and its target dimensions.
cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image,
FivIoOrientation orientation, double *width, double *height);
cairo_matrix_t fiv_io_orientation_matrix(
FivIoOrientation orientation, double width, double height);
void fiv_io_orientation_dimensions(const FivIoImage *image,
FivIoOrientation orientation, double *width, double *height);
@@ -177,4 +216,4 @@ unsigned char *fiv_io_encode_webp(
/// 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(FivIoImage *page, FivIoImage *frame,
FivIoProfile target, const char *path, GError **error);
FivIoProfile *target, const char *path, GError **error);

View File

@@ -18,6 +18,9 @@
#include <gtk/gtk.h>
#include <turbojpeg.h>
#include <stdlib.h>
#include <string.h>
#include "config.h"
// --- Utilities ---------------------------------------------------------------

View File

@@ -5,5 +5,5 @@ if [ "$#" -ne 2 ]; then
fi
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
| curl --silent --show-error --upload-file - https://transfer.sh/image \
| jq --slurp --raw-input --raw-output @uri)"
| curl --silent --show-error --form 'files[]=@-' https://uguu.se/upload \
| jq --raw-output '.files[] | .url | @uri')"

View File

@@ -433,7 +433,10 @@ complete_path(GFile *location, GtkListStore *model)
!info)
break;
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY ||
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY)
continue;
if (g_file_info_has_attribute(info,
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
g_file_info_get_is_hidden(info))
continue;
@@ -534,6 +537,13 @@ on_show_enter_location(
g_signal_connect(entry, "changed",
G_CALLBACK(on_enter_location_changed), self);
GFile *location = fiv_io_model_get_location(self->model);
if (location) {
gchar *parse_name = g_file_get_parse_name(location);
gtk_entry_set_text(GTK_ENTRY(entry), parse_name);
g_free(parse_name);
}
// Can't have it ellipsized and word-wrapped at the same time.
GtkWidget *protocols = gtk_label_new("");
gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);

View File

@@ -100,7 +100,16 @@ mark_thumbnail_lq(cairo_surface_t *surface)
static gchar *
fiv_thumbnail_get_root(void)
{
#ifdef G_OS_WIN32
// We can do better than GLib with FOLDERID_InternetCache,
// and we don't want to place .cache directly in the user's home.
// TODO(p): Register this thumbnail path using the installer:
// https://learn.microsoft.com/en-us/windows/win32/lwef/disk-cleanup
gchar *cache_dir =
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
#else
gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
#endif
gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL);
g_free(cache_dir);
return thumbnails_dir;
@@ -128,9 +137,12 @@ might_be_a_thumbnail(const char *path_or_uri)
static FivIoImage *
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
{
FivIoCmm *cmm = fiv_io_cmm_get_default();
FivIoOpenContext ctx = {
.uri = g_file_get_uri(target),
.screen_profile = fiv_io_profile_new_sRGB(),
// Remember to synchronize changes with adjust_thumbnail().
.cmm = cmm,
.screen_profile = fiv_io_cmm_get_profile_sRGB(cmm),
.screen_dpi = 96,
.first_frame_only = TRUE,
// Only using this array as a redirect.
@@ -171,8 +183,14 @@ adjust_thumbnail(FivIoImage *thumbnail, double row_height)
// Vector images should not have orientation, this should handle them all.
FivIoRenderClosure *closure = thumbnail->render;
if (closure && orientation <= FivIoOrientation0) {
// Remember to synchronize changes with render().
FivIoCmm *cmm = fiv_io_cmm_get_default();
FivIoProfile *screen_profile = fiv_io_cmm_get_profile_sRGB(cmm);
// This API doesn't accept non-uniform scaling; prefer a vertical fit.
FivIoImage *scaled = closure->render(closure, scale_y);
FivIoImage *scaled =
closure->render(closure, cmm, screen_profile, scale_y);
if (screen_profile)
fiv_io_profile_free(screen_profile);
if (scaled)
return scaled;
}
@@ -374,7 +392,7 @@ extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
// The main image's "flip" often matches up, but sometimes doesn't, e.g.:
// - Phase One/H 25/H25_Outdoor_.IIQ
// - Phase One/H 25/H25_IT8.7-2_Card.TIF
*flip = iprc->sizes.flip
*flip = iprc->sizes.flip;
return TRUE;
}
@@ -417,7 +435,7 @@ extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
guint32 *out = (guint32 *) I->data;
const unsigned char *in = image->data;
for (guint64 i = 0; i < image->width * image->height; in += 3)
for (guint64 i = 0; i < (guint64) image->width * image->height; in += 3)
out[i++] = in[0] << 16 | in[1] << 8 | in[2];
I->orientation = extract_libraw_unflip(flip);

View File

@@ -1,7 +1,7 @@
//
// fiv-view.c: image viewing widget
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -24,6 +24,7 @@
#include <math.h>
#include <stdbool.h>
#include <epoxy/gl.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@@ -78,11 +79,15 @@ struct _FivView {
double drag_start[2]; ///< Adjustment values for drag origin
FivIoImage *enhance_swap; ///< Quick swap in/out
FivIoProfile screen_cms_profile; ///< Target colour profile for widget
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
gulong frame_update_connection; ///< GdkFrameClock::update
GdkGLContext *gl_context; ///< OpenGL context
bool gl_initialized; ///< Objects have been created
GLuint gl_program; ///< Linked render program
};
G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0,
@@ -161,6 +166,147 @@ enum {
// Globals are, sadly, the canonical way of storing signal numbers.
static guint view_signals[LAST_SIGNAL];
// --- OpenGL ------------------------------------------------------------------
// While GTK+ 3 technically still supports legacy desktop OpenGL 2.0[1],
// we will pick the 3.3 core profile, which is fairly old by now.
// It doesn't seem to make any sense to go below 3.2.
//
// [1] https://stackoverflow.com/a/37923507/76313
//
// OpenGL ES
//
// Currently, we do not support OpenGL ES at all--it needs its own shaders
// (if only because of different #version statements), and also further analysis
// as to what is our minimum version requirement. While GTK+ 3 can again go
// down as low as OpenGL ES 2.0, this might be too much of a hassle to support.
//
// ES can be forced via GDK_GL=gles, if gdk_gl_context_set_required_version()
// doesn't stand in the way.
//
// Let's not forget that this is a desktop image viewer first and foremost.
static const char *
gl_error_string(GLenum err)
{
switch (err) {
case GL_NO_ERROR:
return "no error";
case GL_CONTEXT_LOST:
return "context lost";
case GL_INVALID_ENUM:
return "invalid enum";
case GL_INVALID_VALUE:
return "invalid value";
case GL_INVALID_OPERATION:
return "invalid operation";
case GL_INVALID_FRAMEBUFFER_OPERATION:
return "invalid framebuffer operation";
case GL_OUT_OF_MEMORY:
return "out of memory";
case GL_STACK_UNDERFLOW:
return "stack underflow";
case GL_STACK_OVERFLOW:
return "stack overflow";
default:
return NULL;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *gl_vertex =
"#version 330\n"
"layout(location = 0) in vec4 position;\n"
"out vec2 coordinates;\n"
"void main() {\n"
"\tcoordinates = position.zw;\n"
"\tgl_Position = vec4(position.xy, 0., 1.);\n"
"}\n";
static const char *gl_fragment =
"#version 330\n"
"in vec2 coordinates;\n"
"layout(location = 0) out vec4 color;\n"
"uniform sampler2D picture;\n"
"uniform bool checkerboard;\n"
"\n"
"vec3 checker() {\n"
"\tvec2 xy = gl_FragCoord.xy / 20.;\n"
"\tif (checkerboard && (int(floor(xy.x) + floor(xy.y)) & 1) == 0)\n"
"\t\treturn vec3(0.98);\n"
"\telse\n"
"\t\treturn vec3(1.00);\n"
"}\n"
"\n"
"void main() {\n"
"\tvec3 c = checker();\n"
"\tvec4 t = texture(picture, coordinates);\n"
"\t// Premultiplied blending with a solid background.\n"
"\t// XXX: This is only correct for linear components.\n"
"\tcolor = vec4(c * (1. - t.a) + t.rgb, 1.);\n"
"}\n";
static GLuint
gl_make_shader(int type, const char *glsl)
{
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &glsl, NULL);
glCompileShader(shader);
GLint status = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (!status) {
GLint len = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
GLchar *buffer = g_malloc0(len + 1);
glGetShaderInfoLog(shader, len, NULL, buffer);
g_warning("GL shader compilation failed: %s", buffer);
g_free(buffer);
glDeleteShader(shader);
return 0;
}
return shader;
}
static GLuint
gl_make_program(void)
{
GLuint vertex = gl_make_shader(GL_VERTEX_SHADER, gl_vertex);
GLuint fragment = gl_make_shader(GL_FRAGMENT_SHADER, gl_fragment);
if (!vertex || !fragment) {
glDeleteShader(vertex);
glDeleteShader(fragment);
return 0;
}
GLuint program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
glDeleteShader(vertex);
glDeleteShader(fragment);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (!status) {
GLint len = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
GLchar *buffer = g_malloc0(len + 1);
glGetProgramInfoLog(program, len, NULL, buffer);
g_warning("GL program linking failed: %s", buffer);
g_free(buffer);
glDeleteProgram(program);
return 0;
}
return program;
}
// -----------------------------------------------------------------------------
static void
on_adjustment_value_changed(
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
@@ -198,12 +344,14 @@ update_adjustments(FivView *self)
if (self->hadjustment) {
gtk_adjustment_configure(self->hadjustment,
gtk_adjustment_get_value(self->hadjustment), 0, dw,
gtk_adjustment_get_value(self->hadjustment),
0, MAX(dw, alloc.width),
alloc.width * 0.1, alloc.width * 0.9, alloc.width);
}
if (self->vadjustment) {
gtk_adjustment_configure(self->vadjustment,
gtk_adjustment_get_value(self->vadjustment), 0, dh,
gtk_adjustment_get_value(self->vadjustment),
0, MAX(dh, alloc.height),
alloc.height * 0.1, alloc.height * 0.9, alloc.height);
}
}
@@ -407,9 +555,16 @@ prescale_page(FivView *self)
// TODO(p): Restart the animation. No vector formats currently animate.
g_return_if_fail(!self->frame_update_connection);
// Optimization, taking into account the workaround in set_scale().
if (!self->page_scaled &&
(self->scale == 1 || self->scale == 0.999999999999999))
return;
// If it fails, the previous frame pointer may become invalid.
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
self->frame = self->page_scaled = closure->render(closure, self->scale);
self->frame = self->page_scaled = closure->render(closure,
self->enable_cms ? fiv_io_cmm_get_default() : NULL,
self->enable_cms ? self->screen_cms_profile : NULL, self->scale);
if (!self->page_scaled)
self->frame = self->page;
}
@@ -453,6 +608,27 @@ out:
//
// Note that Wayland does not have any appropriate protocol, as of writing:
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
static FivIoProfile *
monitor_cms_profile(GdkWindow *root, int num)
{
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;
FivIoProfile *result = NULL;
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)
result = fiv_io_cmm_get_profile(
fiv_io_cmm_get_default(), data, length);
g_free(data);
}
return result;
}
static void
reload_screen_cms_profile(FivView *self, GdkWindow *window)
{
@@ -470,7 +646,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
gchar *data = NULL;
gsize length = 0;
if (g_file_get_contents(path, &data, &length, NULL))
self->screen_cms_profile = fiv_io_profile_new(data, length);
self->screen_cms_profile = fiv_io_cmm_get_profile(
fiv_io_cmm_get_default(), data, length);
g_free(data);
}
g_free(path);
@@ -482,6 +659,7 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
GdkDisplay *display = gdk_window_get_display(window);
GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
int num = -1;
for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; )
@@ -490,24 +668,14 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
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);
}
// Cater to xiccd limitations (agalakhov/xiccd#33).
if (!(self->screen_cms_profile = monitor_cms_profile(root, num)) && num)
self->screen_cms_profile = monitor_cms_profile(root, 0);
out:
if (!self->screen_cms_profile)
self->screen_cms_profile = fiv_io_profile_new_sRGB();
self->screen_cms_profile =
fiv_io_cmm_get_profile_sRGB(fiv_io_cmm_get_default());
}
static void
@@ -541,6 +709,9 @@ fiv_view_realize(GtkWidget *widget)
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
gboolean opengl = g_settings_get_boolean(settings, "opengl");
// Without the following call, or the rendering mode set to "recording",
// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
// creates backing stores using cairo_content_t constants.
@@ -550,20 +721,274 @@ fiv_view_realize(GtkWidget *widget)
// Note that this disables double buffering, and sometimes causes artefacts,
// see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
//
// If GTK+'s OpenGL integration fails to deliver, we need to use the window
// directly, sidestepping the toolkit entirely.
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
// GTK+'s OpenGL integration is terrible, so we may need to use
// the X11 subwindow directly, sidestepping the toolkit entirely.
if (GDK_IS_X11_WINDOW(window) &&
g_settings_get_boolean(settings, "native-view-window"))
gdk_window_ensure_native(window);
g_object_unref(settings);
#endif // GDK_WINDOWING_X11
g_object_unref(settings);
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);
FivView *self = FIV_VIEW(widget);
g_clear_object(&self->gl_context);
if (!opengl)
return;
GError *error = NULL;
GdkGLContext *gl_context = gdk_window_create_gl_context(window, &error);
if (!gl_context) {
g_warning("GL: %s", error->message);
g_error_free(error);
return;
}
gdk_gl_context_set_use_es(gl_context, FALSE);
gdk_gl_context_set_required_version(gl_context, 3, 3);
gdk_gl_context_set_debug_enabled(gl_context, TRUE);
if (!gdk_gl_context_realize(gl_context, &error)) {
g_warning("GL: %s", error->message);
g_error_free(error);
g_object_unref(gl_context);
return;
}
self->gl_context = gl_context;
}
static void GLAPIENTRY
gl_on_message(G_GNUC_UNUSED GLenum source, GLenum type, G_GNUC_UNUSED GLuint id,
G_GNUC_UNUSED GLenum severity, G_GNUC_UNUSED GLsizei length,
const GLchar *message, G_GNUC_UNUSED const void *user_data)
{
if (type == GL_DEBUG_TYPE_ERROR)
g_warning("GL: error: %s", message);
else
g_debug("GL: %s", message);
}
static void
fiv_view_unrealize(GtkWidget *widget)
{
FivView *self = FIV_VIEW(widget);
if (self->gl_context) {
if (self->gl_initialized) {
gdk_gl_context_make_current(self->gl_context);
glDeleteProgram(self->gl_program);
}
if (self->gl_context == gdk_gl_context_get_current())
gdk_gl_context_clear_current();
g_clear_object(&self->gl_context);
}
GTK_WIDGET_CLASS(fiv_view_parent_class)->unrealize(widget);
}
static bool
gl_draw(FivView *self, cairo_t *cr)
{
gdk_gl_context_make_current(self->gl_context);
if (!self->gl_initialized) {
GLuint program = gl_make_program();
if (!program)
return false;
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_BLEND);
if (epoxy_has_gl_extension("GL_ARB_debug_output")) {
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(gl_on_message, NULL);
}
self->gl_program = program;
self->gl_initialized = true;
}
// This limit is always less than that of Cairo/pixman,
// and we'd have to figure out tiling.
GLint max = 0;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
if (max < (GLint) self->frame->width ||
max < (GLint) self->frame->height) {
g_warning("OpenGL max. texture size is too small");
return false;
}
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
int dw = 0, dh = 0, dx = 0, dy = 0;
get_display_dimensions(self, &dw, &dh);
int clipw = dw, cliph = dh;
double x1 = 0., y1 = 0., x2 = 1., y2 = 1.;
if (self->hadjustment)
x1 = floor(gtk_adjustment_get_value(self->hadjustment)) / dw;
if (self->vadjustment)
y1 = floor(gtk_adjustment_get_value(self->vadjustment)) / dh;
if (dw <= allocation.width) {
dx = round((allocation.width - dw) / 2.);
} else {
x2 = x1 + (double) allocation.width / dw;
clipw = allocation.width;
}
if (dh <= allocation.height) {
dy = round((allocation.height - dh) / 2.);
} else {
y2 = y1 + (double) allocation.height / dh;
cliph = allocation.height;
}
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self));
clipw *= scale;
cliph *= scale;
enum { SRC, DEST };
GLuint textures[2] = {};
glGenTextures(2, textures);
// https://stackoverflow.com/questions/25157306 0..1
// GL_TEXTURE_RECTANGLE seems kind-of useful
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
if (self->filter) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
// GL_UNPACK_ALIGNMENT is initially 4, which is fine for these.
// Texture swizzling is OpenGL 3.3.
if (self->frame->format == CAIRO_FORMAT_ARGB32) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
} else if (self->frame->format == CAIRO_FORMAT_RGB24) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
} else if (self->frame->format == CAIRO_FORMAT_RGB30) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV, self->frame->data);
} else {
g_warning("GL: unsupported bitmap format");
}
// GtkGLArea creates textures like this.
glBindTexture(GL_TEXTURE_2D, textures[DEST]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, clipw, cliph, 0, GL_BGRA,
GL_UNSIGNED_BYTE, NULL);
glViewport(0, 0, clipw, cliph);
GLuint vao = 0;
glGenVertexArrays(1, &vao);
GLuint frame_buffer = 0;
glGenFramebuffers(1, &frame_buffer);
glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);
glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[DEST], 0);
glClearColor(0., 0., 0., 1.);
glClear(GL_COLOR_BUFFER_BIT);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
g_warning("GL framebuffer status: %u", status);
glUseProgram(self->gl_program);
GLint position_location = glGetAttribLocation(
self->gl_program, "position");
GLint picture_location = glGetUniformLocation(
self->gl_program, "picture");
GLint checkerboard_location = glGetUniformLocation(
self->gl_program, "checkerboard");
glUniform1i(picture_location, 0);
glUniform1i(checkerboard_location, self->checkerboard);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
// Note that the Y axis is flipped in the table.
double vertices[][4] = {
{-1., -1., x1, y2},
{+1., -1., x2, y2},
{+1., +1., x2, y1},
{-1., +1., x1, y1},
};
cairo_matrix_t matrix = fiv_io_orientation_matrix(self->orientation, 1, 1);
cairo_matrix_transform_point(&matrix, &vertices[0][2], &vertices[0][3]);
cairo_matrix_transform_point(&matrix, &vertices[1][2], &vertices[1][3]);
cairo_matrix_transform_point(&matrix, &vertices[2][2], &vertices[2][3]);
cairo_matrix_transform_point(&matrix, &vertices[3][2], &vertices[3][3]);
GLuint vertex_buffer = 0;
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
glBindVertexArray(vao);
glVertexAttribPointer(position_location,
G_N_ELEMENTS(vertices[0]), GL_DOUBLE, GL_FALSE, sizeof vertices[0], 0);
glEnableVertexAttribArray(position_location);
glDrawArrays(GL_TRIANGLE_FAN, 0, G_N_ELEMENTS(vertices));
glDisableVertexAttribArray(position_location);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glUseProgram(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// XXX: Native GdkWindows send this to the software fallback path.
// XXX: This only reliably alpha blends when using the software fallback,
// such as with a native window, because 7237f5d in GTK+ 3 is a regression.
// (Introduced in 3.24.39, reverted in 3.24.42.)
//
// We had to resort to rendering the checkerboard pattern in the shader.
// Unfortunately, it is hard to retrieve the theme colours from CSS.
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
cairo_translate(cr, dx, dy);
gdk_cairo_draw_from_gl(
cr, window, textures[DEST], GL_TEXTURE, scale, 0, 0, clipw, cliph);
gdk_gl_context_make_current(self->gl_context);
glDeleteBuffers(1, &vertex_buffer);
glDeleteTextures(2, textures);
glDeleteVertexArrays(1, &vao);
glDeleteFramebuffers(1, &frame_buffer);
// TODO(p): Possibly use this clue as a hint to use Cairo rendering.
GLenum err = 0;
while ((err = glGetError()) != GL_NO_ERROR) {
const char *string = gl_error_string(err);
if (string)
g_warning("GL: error: %s", string);
else
g_warning("GL: error: %u", err);
}
gdk_gl_context_clear_current();
return true;
}
static gboolean
@@ -580,8 +1005,10 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
if (!self->image ||
!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
return TRUE;
if (self->gl_context && gl_draw(self, cr))
return TRUE;
int dw, dh;
int dw = 0, dh = 0;
get_display_dimensions(self, &dw, &dh);
double x = 0;
@@ -885,6 +1312,10 @@ switch_page(FivView *self, FivIoImage *page)
{
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
self->frame = self->page = page;
// XXX: When self->scale_to_fit is in effect,
// this uses an old value that may no longer be appropriate,
// resulting in wasted effort.
prescale_page(self);
if (!self->page ||
@@ -1095,7 +1526,7 @@ static gboolean
save_as(FivView *self, FivIoImage *frame)
{
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
FivIoProfile target = NULL;
FivIoProfile *target = NULL;
if (self->enable_cms && (target = self->screen_cms_profile)) {
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
@@ -1271,6 +1702,7 @@ fiv_view_class_init(FivViewClass *klass)
widget_class->map = fiv_view_map;
widget_class->unmap = fiv_view_unmap;
widget_class->realize = fiv_view_realize;
widget_class->unrealize = fiv_view_unrealize;
widget_class->draw = fiv_view_draw;
widget_class->button_press_event = fiv_view_button_press_event;
widget_class->scroll_event = fiv_view_scroll_event;
@@ -1359,6 +1791,7 @@ open_without_swapping_in(FivView *self, const char *uri)
{
FivIoOpenContext ctx = {
.uri = uri,
.cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL,
.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
.screen_dpi = 96, // TODO(p): Try to retrieve it from the screen.
.enhance = self->enhance,

356
fiv.c
View File

@@ -1,7 +1,7 @@
//
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -27,6 +27,7 @@
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef G_OS_WIN32
#include <io.h>
@@ -43,11 +44,6 @@
#include "fiv-thumbnail.h"
#include "fiv-view.h"
#ifdef HAVE_LCMS2_FAST_FLOAT
#include <lcms2.h>
#include <lcms2_fast_float.h>
#endif // HAVE_LCMS2_FAST_FLOAT
// --- Utilities ---------------------------------------------------------------
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
@@ -100,16 +96,16 @@ struct key_section {
static struct key help_keys_general[] = {
{"F1", "Show help"},
{"F10", "Open menu"},
{"<Control>comma", "Preferences"},
{"<Control>question", "Keyboard shortcuts"},
{"q <Control>q", "Quit"},
{"<Control>w", "Quit"},
{"<Primary>comma", "Preferences"},
{"<Primary>question", "Keyboard shortcuts"},
{"q <Primary>q", "Quit"},
{"<Primary>w", "Quit"},
{}
};
static struct key help_keys_navigation[] = {
{"<Control>l", "Open location..."},
{"<Control>n", "Open a new window"},
{"<Primary>l", "Open location..."},
{"<Primary>n", "Open a new window"},
{"<Alt>Left", "Go back in history"},
{"<Alt>Right", "Go forward in history"},
{}
@@ -458,7 +454,7 @@ show_about_dialog(GtkWidget *parent)
GtkWidget *website = gtk_label_new(NULL);
gtk_label_set_selectable(GTK_LABEL(website), TRUE);
const char *url = "https://git.janouch.name/p/" PROJECT_NAME;
const char *url = PROJECT_URL;
gchar *link = g_strdup_printf("<a href='%s'>%s</a>", url, url);
gtk_label_set_markup(GTK_LABEL(website), link);
g_free(link);
@@ -1461,92 +1457,17 @@ toggle_sunlight(void)
g_object_set(settings, property, !set, NULL);
}
// Cursor keys, e.g., simply cannot be bound through accelerators
// (and GtkWidget::keynav-failed would arguably be an awful solution).
//
// GtkBindingSets can be added directly through GtkStyleContext,
// but that would still require setting up action signals on the widget class,
// which is extremely cumbersome. GtkWidget::move-focus has no return value,
// so we can't override that and abort further handling.
//
// Therefore, bind directly to keypresses. Order can be fine-tuned with
// g_signal_connect{,after}(), or overriding the handler and either tactically
// chaining up or using gtk_window_propagate_key_event().
static gboolean
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data)
{
switch (event->state & gtk_accelerator_get_default_mod_mask()) {
case GDK_MOD1_MASK | GDK_SHIFT_MASK:
if (event->keyval == GDK_KEY_D)
toggle_sunlight();
break;
case GDK_CONTROL_MASK:
case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
switch (event->keyval) {
case GDK_KEY_h:
// XXX: Command-H is already occupied on macOS.
gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
return TRUE;
case GDK_KEY_l:
fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
return TRUE;
case GDK_KEY_n:
if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
spawn_uri(g.uri);
else
spawn_uri(g.directory);
return TRUE;
case GDK_KEY_o:
on_open();
return TRUE;
case GDK_KEY_q:
case GDK_KEY_w:
gtk_widget_destroy(g.window);
return TRUE;
case GDK_KEY_question:
show_help_shortcuts();
return TRUE;
case GDK_KEY_comma:
show_preferences(g.window);
return TRUE;
}
break;
case GDK_MOD1_MASK:
switch (event->keyval) {
case GDK_KEY_Left:
go_back();
return TRUE;
case GDK_KEY_Right:
go_forward();
return TRUE;
}
break;
case GDK_SHIFT_MASK:
switch (event->keyval) {
case GDK_KEY_F1:
show_about_dialog(g.window);
return TRUE;
}
break;
case 0:
switch (event->keyval) {
case GDK_KEY_BackSpace:
go_back();
return TRUE;
case GDK_KEY_q:
gtk_widget_destroy(g.window);
return TRUE;
case GDK_KEY_o:
on_open();
return TRUE;
case GDK_KEY_F1:
show_help_contents();
return TRUE;
case GDK_KEY_F11:
case GDK_KEY_f:
toggle_fullscreen();
return TRUE;
}
}
@@ -1561,8 +1482,15 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
gtk_accelerator_parse(accelerator, &key, &mods);
g_free(accelerator);
// TODO(p): See how Unity 7 behaves,
// we might want to keep GtkApplicationWindow:show-menubar then.
gboolean shell_shows_menubar = FALSE;
(void) g_object_get(gtk_settings_get_default(),
"gtk-shell-shows-menubar", &shell_shows_menubar, NULL);
guint mask = gtk_accelerator_get_default_mod_mask();
if (key && event->keyval == key && (event->state & mask) == mods) {
if (key && event->keyval == key && (event->state & mask) == mods &&
!shell_shows_menubar) {
gtk_widget_show(g.menu);
// _gtk_menu_shell_set_keyboard_mode() is private.
@@ -1572,6 +1500,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
return FALSE;
}
// Cursor keys, e.g., simply cannot be bound through accelerators
// (and GtkWidget::keynav-failed would arguably be an awful solution).
//
// GtkBindingSets can be added directly through GtkStyleContext,
// but that would still require setting up action signals on the widget class,
// which is extremely cumbersome. GtkWidget::move-focus has no return value,
// so we can't override that and abort further handling.
//
// Therefore, bind directly to keypresses. Order can be fine-tuned with
// g_signal_connect{,after}(), or overriding the handler and either tactically
// chaining up or using gtk_window_propagate_key_event().
static gboolean
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data)
@@ -1750,6 +1689,9 @@ make_toolbar_radio(const char *label, const char *tooltip)
GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
gtk_widget_set_tooltip_text(button, tooltip);
gtk_widget_set_focus_on_click(button, FALSE);
gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(button), FALSE);
gtk_style_context_add_class(
gtk_widget_get_style_context(button), GTK_STYLE_CLASS_FLAT);
return button;
}
@@ -1811,7 +1753,6 @@ make_browser_toolbar(void)
gtk_radio_button_join_group(radio, last);
last = radio;
}
return browser_toolbar;
}
@@ -2078,41 +2019,178 @@ make_browser_sidebar(FivIoModel *model)
return sidebar;
}
static GtkWidget *
make_menu_bar(void)
// --- Actions -----------------------------------------------------------------
#define ACTION(name) static void on_action_ ## name(void)
ACTION(new_window) {
if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
spawn_uri(g.uri);
else
spawn_uri(g.directory);
}
ACTION(quit) {
gtk_widget_destroy(g.window);
}
ACTION(location) {
fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
}
ACTION(preferences) {
show_preferences(g.window);
}
ACTION(about) {
show_about_dialog(g.window);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct {
const char *name; ///< Unprefixed action name
GCallback callback; ///< Simple callback
const char **accels; ///< NULL-terminated accelerator list
} ActionEntry;
static ActionEntry g_actions[] = {
{"preferences", on_action_preferences,
(const char *[]) {"<Primary>comma", NULL}},
{"new-window", on_action_new_window,
(const char *[]) {"<Primary>n", NULL}},
{"open", on_open,
(const char *[]) {"<Primary>o", "o", NULL}},
{"quit", on_action_quit,
(const char *[]) {"<Primary>q", "<Primary>w", "q", NULL}},
{"toggle-fullscreen", toggle_fullscreen,
(const char *[]) {"F11", "f", NULL}},
{"toggle-sunlight", toggle_sunlight,
(const char *[]) {"<Alt><Shift>d", NULL}},
{"go-back", go_back,
(const char *[]) {"<Alt>Left", "BackSpace", NULL}},
{"go-forward", go_forward,
(const char *[]) {"<Alt>Right", NULL}},
{"go-location", on_action_location,
(const char *[]) {"<Primary>l", NULL}},
{"help", show_help_contents,
(const char *[]) {"F1", NULL}},
{"shortcuts", show_help_shortcuts,
// Similar to win.show-help-overlay in gtkapplication.c.
(const char *[]) {"<Primary>question", "<Primary>F1", NULL}},
{"about", on_action_about,
(const char *[]) {"<Shift>F1", NULL}},
{}
};
static void
dispatch_action(G_GNUC_UNUSED GSimpleAction *action,
G_GNUC_UNUSED GVariant *parameter, gpointer user_data)
{
g.menu = gtk_menu_bar_new();
GCallback callback = user_data;
callback();
}
GtkWidget *item_quit = gtk_menu_item_new_with_mnemonic("_Quit");
g_signal_connect_swapped(item_quit, "activate",
G_CALLBACK(gtk_widget_destroy), g.window);
static void
set_up_action(GtkApplication *app, const ActionEntry *a)
{
GSimpleAction *action = g_simple_action_new(a->name, NULL);
g_signal_connect(action, "activate",
G_CALLBACK(dispatch_action), a->callback);
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action));
g_object_unref(action);
GtkWidget *menu_file = gtk_menu_new();
gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit);
GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_file), menu_file);
gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_file);
gchar *full_name = g_strdup_printf("app.%s", a->name);
gtk_application_set_accels_for_action(app, full_name, a->accels);
g_free(full_name);
}
GtkWidget *item_contents = gtk_menu_item_new_with_mnemonic("_Contents");
g_signal_connect_swapped(item_contents, "activate",
G_CALLBACK(show_help_contents), NULL);
GtkWidget *item_shortcuts =
gtk_menu_item_new_with_mnemonic("_Keyboard Shortcuts");
g_signal_connect_swapped(item_shortcuts, "activate",
G_CALLBACK(show_help_shortcuts), NULL);
GtkWidget *item_about = gtk_menu_item_new_with_mnemonic("_About");
g_signal_connect_swapped(item_about, "activate",
G_CALLBACK(show_about_dialog), g.window);
// --- Menu --------------------------------------------------------------------
GtkWidget *menu_help = gtk_menu_new();
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents);
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts);
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about);
GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_help), menu_help);
gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_help);
typedef struct {
const char *label; ///< Label, with a mnemonic
const char *action; ///< Prefixed action name
gboolean macos; ///< Show in the macOS global menu?
} MenuItem;
typedef struct {
const char *label; ///< Label, with a mnemonic
const MenuItem *items; ///< ""-sectioned menu items
} MenuRoot;
// We're single-instance, skip the "win" namespace for simplicity.
static MenuRoot g_menu[] = {
{"_File", (MenuItem[]) {
{"_New Window", "app.new-window", TRUE},
{"_Open...", "app.open", TRUE},
{"", NULL, TRUE},
{"_Quit", "app.quit", FALSE},
{}
}},
{"_Go", (MenuItem[]) {
{"_Back", "app.go-back", TRUE},
{"_Forward", "app.go-forward", TRUE},
{"", NULL, TRUE},
{"_Location...", "app.go-location", TRUE},
{}
}},
{"_Help", (MenuItem[]) {
{"_Contents", "app.help", TRUE},
{"_Keyboard Shortcuts", "app.shortcuts", TRUE},
{"_About", "app.about", FALSE},
{}
}},
{}
};
static GMenuModel *
make_submenu(const MenuItem *items)
{
GMenu *menu = g_menu_new();
while (items->label) {
GMenu *section = g_menu_new();
for (; items->label; items++) {
// Empty strings are interpreted as separators.
if (!*items->label) {
items++;
break;
}
GMenuItem *subitem = g_menu_item_new(items->label, items->action);
if (!items->macos) {
g_menu_item_set_attribute(
subitem, "hidden-when", "s", "macos-menubar");
}
g_menu_append_item(section, subitem);
g_object_unref(subitem);
}
g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
g_object_unref(section);
}
return G_MENU_MODEL(menu);
}
static GMenuModel *
make_menu_model(void)
{
GMenu *menu = g_menu_new();
for (const MenuRoot *root = g_menu; root->label; root++) {
GMenuModel *submenu = make_submenu(root->items);
g_menu_append_submenu(menu, root->label, submenu);
g_object_unref(submenu);
}
return G_MENU_MODEL(menu);
}
static GtkWidget *
make_menu_bar(GMenuModel *model)
{
g.menu = gtk_menu_bar_new_from_model(model);
// Don't let it take up space by default. Firefox sets a precedent here.
// (gtk_application_window_set_show_menubar() doesn't seem viable for use
// for this purpose.)
gtk_widget_show_all(g.menu);
gtk_widget_set_no_show_all(g.menu, TRUE);
gtk_widget_hide(g.menu);
@@ -2120,6 +2198,8 @@ make_menu_bar(void)
return g.menu;
}
// --- Application -------------------------------------------------------------
// This is incredibly broken https://stackoverflow.com/a/51054396/76313
// thus resolving the problem using overlaps.
// We're trying to be universal for light and dark themes both. It's hard.
@@ -2128,7 +2208,10 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
fiv-view, fiv-browser { background: @content_view_bg; } \
placessidebar.fiv box > separator { margin: 4px 0; } \
placessidebar.fiv row { min-height: 2em; } \
.fiv-toolbar button { padding-left: 0; padding-right: 0; } \
.fiv-toolbar button.text-button { \
padding-left: 4px; padding-right: 4px; } \
.fiv-toolbar > button:first-child { padding-left: 4px; } \
.fiv-toolbar > button:last-child { padding-right: 4px; } \
.fiv-toolbar separator { \
@@ -2304,10 +2387,27 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
g_signal_connect(g.window, "window-state-event",
G_CALLBACK(on_window_state_event), NULL);
for (const ActionEntry *a = g_actions; a->name; a++)
set_up_action(GTK_APPLICATION(app), a);
// GtkApplicationWindow overrides GtkContainer/GtkWidget virtual methods
// so that it has the menu bar as an extra child (if it so decides).
// However, we currently want this menu bar to only show on a key press,
// and to hide as soon as it's no longer being used.
// Messing with the window's internal state seems at best quirky,
// so we'll manage the menu entirely by ourselves.
gtk_application_window_set_show_menubar(
GTK_APPLICATION_WINDOW(g.window), FALSE);
GMenuModel *menu = make_menu_model();
gtk_application_set_menubar(GTK_APPLICATION(app), menu);
// The default "app menu" is good, in particular for macOS, so keep it.
GtkWidget *menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar());
gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar(menu));
gtk_container_add(GTK_CONTAINER(menu_box), g.stack);
gtk_container_add(GTK_CONTAINER(g.window), menu_box);
g_object_unref(menu);
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
if (g_settings_get_boolean(settings, "dark-theme"))
@@ -2352,7 +2452,7 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
}
static struct {
gboolean browse, extract_thumbnail;
gboolean browse, collection, extract_thumbnail;
gchar **args, *thumbnail_size, *thumbnail_size_search;
} o;
@@ -2362,12 +2462,12 @@ on_app_activate(
{
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
// interpret multiple command line arguments differently, as a collection.
// However, single-element collections are unrepresentable this way.
// Should we allow multiple targets only in a special new mode?
// However, single-element collections are unrepresentable this way,
// so we have a switch to enforce it.
g.files_index = -1;
if (o.args) {
const gchar *target = *o.args;
if (o.args[1]) {
if (o.args[1] || o.collection) {
fiv_collection_reload(o.args);
target = FIV_COLLECTION_SCHEME ":/";
}
@@ -2483,11 +2583,6 @@ on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
return 0;
}
// TODO(p): Use Little CMS with contexts instead.
#ifdef HAVE_LCMS2_FAST_FLOAT
cmsPlugin(cmsFastFloatExtensions());
#endif // HAVE_LCMS2_FAST_FLOAT
// Normalize all arguments to URIs, and run thumbnailing modes first.
for (gsize i = 0; o.args && o.args[i]; i++) {
GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
@@ -2518,6 +2613,9 @@ main(int argc, char *argv[])
{"browse", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &o.browse,
"Start in filesystem browsing mode", NULL},
{"collection", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &o.collection,
"Always put arguments in a collection (implies --browse)", NULL},
{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, NULL,
"Invalidate the wide thumbnail cache", NULL},

View File

@@ -17,6 +17,13 @@
double buffering.
</description>
</key>
<key name='opengl' type='b'>
<default>false</default>
<summary>Use experimental OpenGL rendering</summary>
<description>
OpenGL within GTK+ is highly problematic--you don't want this.
</description>
</key>
<key name='dark-theme' type='b'>
<default>false</default>
<summary>Use a dark theme variant on start-up</summary>

71
fiv.wxs.in Normal file
View File

@@ -0,0 +1,71 @@
<?xml version='1.0' encoding='utf-8'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
<?define FullName = "@ProjectName@ @ProjectVersion@" ?>
<?if $(sys.BUILDARCH) = x64 ?>
<?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?define ProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif?>
<Product Id='*'
Name='$(var.FullName)'
UpgradeCode='a3e64e2d-4310-4c5f-8562-bb0e0b3e0a53'
Language='1033'
Codepage='1252'
Version='@ProjectVersion@'
Manufacturer='Premysl Eric Janouch'>
<Package Id='*'
Keywords='Installer,Image,Viewer'
Description='$(var.FullName) Installer'
Manufacturer='Premysl Eric Janouch'
InstallerVersion='200'
Compressed='yes'
Languages='1033'
SummaryCodepage='1252' />
<Media Id='1' Cabinet='data.cab' EmbedCab='yes' />
<Icon Id='fiv.ico' SourceFile='fiv.ico' />
<Property Id='ARPPRODUCTICON' Value='fiv.ico' />
<Property Id='ARPURLINFOABOUT' Value='@ProjectURL@' />
<UIRef Id='WixUI_Minimal' />
<!-- This isn't supported by msitools, but is necessary for WiX.
<WixVariable Id='WixUILicenseRtf' Value='License.rtf' />
-->
<Directory Id='TARGETDIR' Name='SourceDir'>
<Directory Id='$(var.ProgramFilesFolder)'>
<Directory Id='INSTALLDIR' Name='$(var.FullName)' />
</Directory>
<Directory Id='ProgramMenuFolder'>
<Directory Id='ProgramMenuDir' Name='$(var.FullName)' />
</Directory>
<Directory Id='DesktopFolder' />
</Directory>
<DirectoryRef Id='ProgramMenuDir'>
<Component Id='ProgramMenuDir' Guid='*'>
<Shortcut Id='ProgramsMenuShortcut'
Name='@ProjectName@'
Target='[INSTALLDIR]\fiv.exe'
WorkingDirectory='INSTALLDIR'
Arguments='"%USERPROFILE%"'
Icon='fiv.ico' />
<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
<RegistryValue Root='HKCU'
Key='Software\[Manufacturer]\[ProductName]'
Type='string'
Value=''
KeyPath='yes' />
</Component>
</DirectoryRef>
<Feature Id='Complete' Level='1'>
<ComponentGroupRef Id='CG.fiv' />
<ComponentRef Id='ProgramMenuDir' />
</Feature>
</Product>
</Wix>

View File

@@ -1,7 +1,7 @@
# vim: noet ts=4 sts=4 sw=4:
project('fiv', 'c',
default_options : ['c_std=gnu99', 'warning_level=2'],
version : '0.1.0',
version : '1.0.0',
meson_version : '>=0.57')
cc = meson.get_compiler('c')
@@ -36,7 +36,9 @@ gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
dependencies = [
dependency('gtk+-3.0'),
dependency('pixman-1'),
dependency('epoxy'),
dependency('libjpeg'),
dependency('libturbojpeg'),
dependency('libwebp'),
dependency('libwebpdemux'),
@@ -93,11 +95,13 @@ endif
# XXX: https://github.com/mesonbuild/meson/issues/825
docdir = get_option('datadir') / 'doc' / meson.project_name()
application_ns = 'name.janouch.'
application_url = 'https://janouch.name/p/' + meson.project_name()
conf = configuration_data()
conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
conf.set_quoted('PROJECT_NS', application_ns)
conf.set_quoted('PROJECT_URL', application_url)
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
if win32
conf.set_quoted('PROJECT_DOCDIR', docdir)
@@ -161,7 +165,8 @@ tiff_tables = custom_target('tiff-tables.h',
)
desktops = ['fiv.desktop', 'fiv-browse.desktop']
iolib = static_library('fiv-io', 'fiv-io.c', 'xdg.c', tiff_tables,
iolib = static_library('fiv-io', 'fiv-io.c', 'fiv-io-cmm.c', 'xdg.c',
tiff_tables, config,
dependencies : dependencies).extract_all_objects(recursive : true)
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
@@ -183,13 +188,13 @@ jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
)
if get_option('tools').enabled()
# libjq 1.6 lacks a pkg-config file, and there is no release in sight.
# libjq 1.6 is required.
# libjq has only received a pkg-config file in version 1.7.
# libjq >= 1.6 is required.
tools_dependencies = [
cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
tools_c_args = cc.get_supported_arguments(
'-Wno-unused-function', '-Wno-unused-parameter')
foreach tool : ['info', 'pnginfo', 'rawinfo']
foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
executable(tool, 'tools/' + tool + '.c', tiff_tables,
dependencies : tools_dependencies,
c_args: tools_c_args)
@@ -215,6 +220,7 @@ endforeach
# For the purposes of development: make the program find its GSettings schemas.
gnome.compile_schemas(depend_files : files(gsettings_schemas))
gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
# Meson is broken on Windows and removes the backslashes, so this ends up empty.
symbolics = run_command(find_program('sed', required : false, disabler : true),
@@ -333,11 +339,43 @@ if not win32
if not meson.is_cross_build()
meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
endif
# Quick and dirty package generation, lacking dependencies.
packaging = configuration_data({
'name' : meson.project_name(),
'version' : meson.project_version(),
'summary' : 'Image viewer',
'author' : 'Přemysl Eric Janouch',
})
subdir('submodules/liberty/meson/packaging')
elif meson.is_cross_build()
# Note that even compiling /from within MSYS2/ can still be a cross-build.
msys2_root = meson.get_external_property('msys2_root')
meson.add_install_script('msys2-install.sh', msys2_root)
wxs = configure_file(
input : 'fiv.wxs.in',
output : 'fiv.wxs',
configuration : configuration_data({
'ProjectName' : meson.project_name(),
'ProjectVersion' : meson.project_version(),
'ProjectURL' : application_url,
}),
)
msi = meson.project_name() + '-' + meson.project_version() + \
'-' + host_machine.cpu() + '.msi'
custom_target('package',
output : msi,
command : [meson.current_source_dir() / 'msys2-package.sh',
host_machine.cpu(), msi, wxs],
env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
console : true,
build_always_stale : true,
build_by_default : false,
)
# This is the minimum to run targets from msys2-configure.sh builds.
meson.add_devenv({
'WINEPATH' : msys2_root / 'bin',

View File

@@ -19,7 +19,7 @@ then
wine64() { "$@"; }
awk() { command awk -v RS='\r?\n' "$@"; }
pacman -S --needed libarchive $pkg-ca-certificates $pkg-gcc $pkg-icoutils \
$pkg-librsvg $pkg-meson $pkg-pkgconf
$pkg-librsvg $pkg-meson $pkg-msitools $pkg-pkgconf
fi
status() {
@@ -46,7 +46,8 @@ fetch() {
} BEGIN { while ((getline < "db.tsv") > 0) {
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | while IFS= read -r name
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
while IFS= read -r name
do
status Fetching "$name"
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
@@ -69,15 +70,20 @@ extract() {
for subdir in *
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
done
for i in packages/*
do bsdtar -xf "$i" --strip-components 1 \
while IFS= read -r name
do bsdtar -xf "packages/$name" --strip-components 1 \
--exclude '*/share/man' --exclude '*/share/doc'
done
done < db.want
# Don't require Perl, which may not exist anymore on i686:
# https://github.com/msys2/MINGW-packages/pull/20085
if [ -d lib/perl5 ]
then
bsdtar -xf exiftool.tar.gz
mv Image-ExifTool-*/exiftool bin
mv Image-ExifTool-*/lib/* lib/perl5/site_perl
rm -rf Image-ExifTool-*
fi
}
configure() {
@@ -120,13 +126,14 @@ setup() {
endian = 'little'
EOF
meson setup --buildtype=debugoptimized --prefix="$packagedir" \
meson setup --buildtype=debugoptimized --prefix=/ \
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
}
# Note: you may need GNU coreutils realpath for non-existent build directories
# (macOS and busybox will probably not work).
sourcedir=$(realpath "${2:-$(dirname "$0")}")
builddir=$(realpath "${1:-builddir}")
packagedir=$builddir/package
toolchain=$builddir/msys2-cross-toolchain.meson
# This directory name matches the prefix in .pc files, so we don't need to
@@ -136,7 +143,7 @@ msys2_root=$builddir$prefix
mkdir -p "$msys2_root"
cd "$msys2_root"
dbsync
fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-perl \
fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-libjxl $pkg-perl \
$pkg-perl-win32-api $pkg-libwinpthread-git # Because we don't do "provides"?
verify
extract

View File

@@ -12,23 +12,24 @@ fi
# Copy binaries we directly or indirectly depend on.
cp -p "$msys2_root"/bin/*.dll .
cp -p "$msys2_root"/bin/wperl.exe .
cp -p "$msys2_root"/bin/exiftool .
cp -p "$msys2_root"/bin/wperl.exe . || :
cp -p "$msys2_root"/bin/exiftool . || :
# The console helper is only useful for debug builds.
cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
cp -pR "$msys2_root"/etc/ .
mkdir -p lib
cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib
cp -pR "$msys2_root"/lib/perl5/ lib
cp -pR "$msys2_root"/lib/perl5/ lib || :
mkdir -p share/glib-2.0/schemas
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
mkdir -p share
cp -pR "$msys2_root"/share/mime/ share
mkdir -p share/icons
cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons
mkdir -p share/icons/hicolor
cp -p "$msys2_root"/share/icons/hicolor/index.theme share/icons/hicolor
mkdir -p share/mime
# GIO doesn't use the database on Windows, this subset is for us.
find "$msys2_root"/share/mime/ -maxdepth 1 -type f -exec cp -p {} share/mime \;
# Remove unreferenced libraries.
find lib -name '*.a' -exec rm -- {} +

35
msys2-package.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/sh -e
export LC_ALL=C
cd "$MESON_BUILD_ROOT"
arch=$1 msi=$2 files=package-files.wxs
destdir=$(pwd)/package/${msi%.*}
shift 2
# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
[ "$arch" = "x86" ] || arch=x64
rm -rf "$destdir"
meson install --destdir "$destdir"
txt2rtf() {
LC_ALL=C.UTF-8 iconv -f utf-8 -t ascii//translit "$@" | awk 'BEGIN {
print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
print "\\f0\\fs24{\\pard\\sa240"
} {
gsub(/\\/, "\\\\"); gsub(/[{]/, "\\{"); gsub(/[}]/, "\\}")
if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
else { print prefix $0; prefix = " " }
} END {
print "\\par}}"
}'
}
# msitools have this filename hardcoded in UI files, and it's required.
txt2rtf "$MESON_SOURCE_ROOT/LICENSE" > License.rtf
find "$destdir" -type f \
| wixl-heat --prefix "$destdir/" --directory-ref INSTALLDIR \
--component-group CG.fiv --var var.SourceDir > "$files"
wixl --verbose --arch "$arch" -D SourceDir="$destdir" --ext ui \
--output "$msi" "$@" "$files"

View File

@@ -1,8 +1,8 @@
[wrap-file]
directory = jpeg-quantsmooth-1.20210408
source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20210408.tar.gz
source_filename = jpeg-quantsmooth-1.20210408.tar.gz
source_hash = 5937ca26db33888cab8638c1a8dc7a367a953bd0857ceb1290d5abc6febf3116
directory = jpeg-quantsmooth-1.20230818
source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20230818.tar.gz
source_filename = jpeg-quantsmooth-1.20230818.tar.gz
source_hash = ff9a62e8560851648c60d84b3d97ebd9769f01ce6b995779e071d19a759eca06
patch_directory = libjpegqs
[provide]

View File

@@ -1,8 +1,6 @@
# vim: noet ts=4 sts=4 sw=4:
project('jpeg-qs', 'c')
add_project_arguments(meson.get_compiler('c')
.get_supported_arguments('-Wno-misleading-indentation'),
'-DWITH_LOG', language : 'c')
add_project_arguments('-DWITH_LOG', language : 'c')
deps = [
dependency('libjpeg'),

210
tools/hotpixels.c Normal file
View File

@@ -0,0 +1,210 @@
//
// hotpixels.c: look for hot pixels in raw image files
//
// Usage: pass a bunch of raw photo images taken with the lens cap on at,
// e.g., ISO 8000-12800 @ 1/20-1/60, and store the resulting file as,
// e.g., Nikon D7500.badpixels, which can then be directly used by Rawtherapee.
//
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include <libraw.h>
#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
#error LibRaw 0.21.0 or newer is required.
#endif
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void *
xreallocarray(void *o, size_t n, size_t m)
{
if (m && n > SIZE_MAX / m) {
fprintf(stderr, "xreallocarray: %s\n", strerror(ENOMEM));
exit(EXIT_FAILURE);
}
void *p = realloc(o, n * m);
if (!p && n && m) {
fprintf(stderr, "xreallocarray: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
return p;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct coord { ushort x, y; };
static bool
coord_equals(struct coord a, struct coord b)
{
return a.x == b.x && a.y == b.y;
}
static int
coord_cmp(const void *a, const void *b)
{
const struct coord *ca = (const struct coord *) a;
const struct coord *cb = (const struct coord *) b;
return ca->y != cb->y
? (int) ca->y - (int) cb->y
: (int) ca->x - (int) cb->x;
}
struct candidates {
struct coord *xy;
size_t len;
size_t alloc;
};
static void
candidates_add(struct candidates *c, ushort x, ushort y)
{
if (c->len == c->alloc) {
c->alloc += 64;
c->xy = xreallocarray(c->xy, sizeof *c->xy, c->alloc);
}
c->xy[c->len++] = (struct coord) {x, y};
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// A stretch of zeroes that is assumed to mean start of outliers.
#define SPAN 10
static const char *
process_raw(struct candidates *c, const uint8_t *p, size_t len)
{
libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
if (!iprc)
return "failed to obtain a LibRaw handle";
int err = 0;
if ((err = libraw_open_buffer(iprc, p, len)) ||
(err = libraw_unpack(iprc))) {
libraw_close(iprc);
return libraw_strerror(err);
}
if (!iprc->rawdata.raw_image) {
libraw_close(iprc);
return "only Bayer raws are supported, not Foveon";
}
// Make a histogram.
uint64_t bins[USHRT_MAX] = {};
for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
ushort y = iprc->sizes.top_margin + yy;
ushort x = iprc->sizes.left_margin + xx;
bins[iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x]]++;
}
}
// Detecting outliers is not completely straight-forward,
// it may help to see the histogram.
if (getenv("HOTPIXELS_HISTOGRAM")) {
for (ushort i = 0; i < USHRT_MAX; i++)
fprintf(stderr, "%u ", (unsigned) bins[i]);
fputc('\n', stderr);
}
// Go to the first non-zero pixel value.
size_t last = 0;
for (; last < USHRT_MAX; last++)
if (bins[last])
break;
// Find the last pixel value we assume to not be hot.
for (; last < USHRT_MAX - SPAN - 1; last++) {
uint64_t nonzero = 0;
for (int i = 1; i <= SPAN; i++)
nonzero += bins[last + i];
if (!nonzero)
break;
}
// Store coordinates for all pixels above that value.
for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
ushort y = iprc->sizes.top_margin + yy;
ushort x = iprc->sizes.left_margin + xx;
if (iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x] > last)
candidates_add(c, xx, yy);
}
}
libraw_close(iprc);
return NULL;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *
do_file(struct candidates *c, const char *filename)
{
FILE *fp = fopen(filename, "rb");
if (!fp)
return strerror(errno);
uint8_t *data = NULL, buf[256 << 10];
size_t n, len = 0;
while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
data = xreallocarray(data, len + n, 1);
memcpy(data + len, buf, n);
len += n;
}
const char *err = ferror(fp)
? strerror(errno)
: process_raw(c, data, len);
fclose(fp);
free(data);
return err;
}
int
main(int argc, char *argv[])
{
struct candidates c = {};
for (int i = 1; i < argc; i++) {
const char *filename = argv[i], *err = do_file(&c, filename);
if (err) {
fprintf(stderr, "%s: %s\n", filename, err);
return EXIT_FAILURE;
}
}
qsort(c.xy, c.len, sizeof *c.xy, coord_cmp);
// If it is detected in all passed photos, it is probably indeed bad.
int count = 1;
for (size_t i = 1; i <= c.len; i++) {
if (i != c.len && coord_equals(c.xy[i - 1], c.xy[i])) {
count++;
continue;
}
if (count == argc - 1)
printf("%u %u\n", c.xy[i - 1].x, c.xy[i - 1].y);
count = 1;
}
return 0;
}

View File

@@ -123,6 +123,39 @@ parse_exif_subifds(const struct tiffer *T, struct tiffer_entry *entry,
return a;
}
// Implemented partially, out of curiosity--it is not particularly useful,
// because there is a ton more parsing to do here.
static bool
parse_exif_makernote(jv *v, const struct tiffer_entry *entry)
{
if (!getenv("INFO_MAKERNOTE") ||
entry->tag != Exif_MakerNote || entry->type != TIFFER_UNDEFINED)
return false;
struct tiffer T = {};
if (entry->remaining_count >= 16 &&
!memcmp(entry->p, "Nikon\x00\x02", 7) &&
tiffer_init(&T, entry->p + 10, entry->remaining_count - 10) &&
tiffer_next_ifd(&T)) {
*v = parse_exif_ifd(&T, NULL);
return true;
}
if (entry->remaining_count >= 16 &&
!memcmp(entry->p, "Apple iOS\x00\x00\x01MM", 14)) {
T.un = &tiffer_unbe;
T.begin = T.p = entry->p + 14;
T.end = entry->p + entry->remaining_count - 14;
T.remaining_fields = 0;
struct tiffer subT = {};
if (tiffer_subifd(&T, 0, &subT)) {
*v = parse_exif_ifd(&subT, NULL);
return true;
}
}
return false;
}
static jv
parse_exif_ascii(struct tiffer_entry *entry)
{
@@ -198,6 +231,8 @@ parse_exif_entry(jv o, const struct tiffer *T, struct tiffer_entry *entry,
v = parse_exif_subifds(T, entry, subentries);
} else if (entry->type == TIFFER_ASCII) {
v = parse_exif_extract_sole_array_element(parse_exif_ascii(entry));
} else if (info_begin == exif_entries && parse_exif_makernote(&v, entry)) {
// Already processed.
} else if (entry->type == TIFFER_UNDEFINED && !info->values) {
// Several Exif entries of UNDEFINED type contain single-byte numbers.
v = parse_exif_undefined(entry);

3
xdg.c
View File

@@ -17,6 +17,9 @@
#include <glib.h>
#include <stdlib.h>
#include <string.h>
/// Add `element` to the `output` set. `relation` is a map of sets of strings
/// defining is-a relations, and is traversed recursively.
static void