Compare commits
36 Commits
84269b2ba2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
9de3747328
|
|||
|
eb65d8582f
|
|||
|
c93a1fb9b7
|
|||
|
577de6bfb7
|
|||
|
690e60cd74
|
|||
|
a7ff9f220d
|
|||
|
2234fd008d
|
|||
|
0fceaf7728
|
|||
|
c46fc73c34
|
|||
|
bdd18fc898
|
|||
|
cf6ded1d03
|
|||
|
3bea18708f
|
|||
|
ed8ba147ba
|
|||
|
c221a00c33
|
|||
|
192ffa0de9
|
|||
|
bac9fce4e0
|
|||
|
2e9ea9b4e2
|
|||
|
b34fe63198
|
|||
|
3c8ddcaf26
|
|||
|
e3ec07a19f
|
|||
|
e57364cd97
|
|||
|
7330f07dd7
|
|||
|
d68e09525c
|
|||
|
115a7bab0f
|
|||
|
91538aaba5
|
|||
|
c214e668d9
|
|||
|
a5ebc697ad
|
|||
|
9ca18f52d5
|
|||
|
604594a8f1
|
|||
|
9acab00bcc
|
|||
|
ae8dc3070a
|
|||
|
3c8a280546
|
|||
|
96189b70b8
|
|||
|
67433f3776
|
|||
|
c1418c7462
|
|||
|
935506b120
|
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2021 - 2024, 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
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
|||||||
17
README.adoc
17
README.adoc
@@ -2,7 +2,7 @@ fiv
|
|||||||
===
|
===
|
||||||
|
|
||||||
'fiv' is a slightly unconventional, general-purpose image browser and viewer
|
'fiv' is a slightly unconventional, general-purpose image browser and viewer
|
||||||
for Linux and Windows (macOS still has major issues).
|
for Linux and Windows (macOS also kind of works).
|
||||||
|
|
||||||
image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
|
image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ Features
|
|||||||
photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
|
photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
|
||||||
modules manage to load.
|
modules manage to load.
|
||||||
- Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
|
- 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.
|
- 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
|
- Can keep the zoom and position when browsing, to help with comparing
|
||||||
zoomed-in images.
|
zoomed-in images.
|
||||||
@@ -38,12 +38,15 @@ You can get a package with the latest development version using Arch Linux's
|
|||||||
https://aur.archlinux.org/packages/fiv-git[AUR],
|
https://aur.archlinux.org/packages/fiv-git[AUR],
|
||||||
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
|
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
|
Building and Running
|
||||||
--------------------
|
--------------------
|
||||||
Build-only dependencies:
|
Build-only dependencies:
|
||||||
Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
|
Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
|
||||||
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
|
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,
|
Optional dependencies: lcms2, Little CMS fast float plugin,
|
||||||
LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
|
LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
|
||||||
resvg (unstable API, needs to be requested explicitly) +
|
resvg (unstable API, needs to be requested explicitly) +
|
||||||
@@ -88,6 +91,14 @@ _mingw-w64-lcms2_ with the following change:
|
|||||||
|
|
||||||
sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
|
sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
|
||||||
|
|
||||||
|
macOS
|
||||||
|
~~~~~
|
||||||
|
Support for this operating system isn't as good.
|
||||||
|
If you install Homebrew, you can get an application bundle with:
|
||||||
|
|
||||||
|
$ sh -e macos-configure.sh builddir
|
||||||
|
$ meson install -C builddir
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
For information concerning usage, refer to link:docs/fiv.html[the user guide],
|
For information concerning usage, refer to link:docs/fiv.html[the user guide],
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ Options
|
|||||||
handler to implement the "Open Containing Folder" feature of certain
|
handler to implement the "Open Containing Folder" feature of certain
|
||||||
applications.
|
applications.
|
||||||
|
|
||||||
|
*--collection*::
|
||||||
|
Always put arguments in a virtual directory, even when only one is passed.
|
||||||
|
Implies *--browse*.
|
||||||
|
|
||||||
*--help-all*::
|
*--help-all*::
|
||||||
Show the full list of options, including those provided by GTK+.
|
Show the full list of options, including those provided by GTK+.
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ q:lang(en):after { content: "’"; }
|
|||||||
<p class="details">
|
<p class="details">
|
||||||
<span id="author">Přemysl Eric Janouch</span><br>
|
<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="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>
|
<span id="revdate">2023-04-17</span>
|
||||||
|
|
||||||
<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
|
<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
|
||||||
|
|||||||
BIN
docs/fiv.webp
BIN
docs/fiv.webp
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 194 KiB |
123
fiv-browser.c
123
fiv-browser.c
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// fiv-browser.c: filesystem browsing widget
|
// 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
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@@ -92,7 +92,8 @@ struct _FivBrowser {
|
|||||||
|
|
||||||
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
|
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
|
||||||
size_t thumbnailers_len; ///< Thumbnailers array size
|
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
|
GdkCursor *pointer; ///< Cached pointer cursor
|
||||||
cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners
|
cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners
|
||||||
@@ -742,7 +743,7 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
|
|||||||
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
|
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
|
||||||
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
|
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
|
||||||
(void *) (intptr_t) 1, NULL);
|
(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);
|
entry_set_surface_user_data(entry);
|
||||||
@@ -796,13 +797,21 @@ on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
|
|||||||
thumbnailer_next(t);
|
thumbnailer_next(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(p): Try to keep the minions alive (stdout will be a problem).
|
||||||
static gboolean
|
static gboolean
|
||||||
thumbnailer_next(Thumbnailer *t)
|
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;
|
FivBrowser *self = t->self;
|
||||||
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue)))
|
do {
|
||||||
return FALSE;
|
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:
|
// Case analysis:
|
||||||
// - We haven't found any thumbnail for the entry at all
|
// - We haven't found any thumbnail for the entry at all
|
||||||
@@ -819,9 +828,18 @@ thumbnailer_next(Thumbnailer *t)
|
|||||||
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
|
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
|
||||||
"--", uri, NULL};
|
"--", 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;
|
GError *error = NULL;
|
||||||
t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower,
|
t->minion = g_subprocess_launcher_spawnv(
|
||||||
G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
|
launcher, t->target->icon ? argv_faster : argv_slower, &error);
|
||||||
|
g_object_unref(launcher);
|
||||||
if (error) {
|
if (error) {
|
||||||
g_warning("%s", error->message);
|
g_warning("%s", error->message);
|
||||||
g_error_free(error);
|
g_error_free(error);
|
||||||
@@ -839,7 +857,8 @@ thumbnailer_next(Thumbnailer *t)
|
|||||||
static void
|
static void
|
||||||
thumbnailers_abort(FivBrowser *self)
|
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++) {
|
for (size_t i = 0; i < self->thumbnailers_len; i++) {
|
||||||
Thumbnailer *t = self->thumbnailers + i;
|
Thumbnailer *t = self->thumbnailers + i;
|
||||||
@@ -855,35 +874,35 @@ thumbnailers_abort(FivBrowser *self)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
thumbnailers_start(FivBrowser *self)
|
thumbnailers_enqueue(FivBrowser *self, Entry *entry)
|
||||||
{
|
{
|
||||||
thumbnailers_abort(self);
|
if (!entry->removed) {
|
||||||
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->icon)
|
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(
|
else if (cairo_surface_get_user_data(
|
||||||
entry->thumbnail, &fiv_thumbnail_key_lq))
|
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++) {
|
for (size_t i = 0; i < self->thumbnailers_len; i++) {
|
||||||
if (!thumbnailer_next(self->thumbnailers + i))
|
if (!thumbnailer_next(self->thumbnailers + i))
|
||||||
break;
|
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 -------------------------------------------------------------
|
// --- Boilerplate -------------------------------------------------------------
|
||||||
|
|
||||||
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
|
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
|
||||||
@@ -1004,7 +1023,7 @@ set_item_size(FivBrowser *self, FivThumbnailSize size)
|
|||||||
|
|
||||||
g_hash_table_remove_all(self->thumbnail_cache);
|
g_hash_table_remove_all(self->thumbnail_cache);
|
||||||
reload_thumbnails(self);
|
reload_thumbnails(self);
|
||||||
thumbnailers_start(self);
|
thumbnailers_restart(self);
|
||||||
|
|
||||||
g_object_notify_by_pspec(
|
g_object_notify_by_pspec(
|
||||||
G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
|
G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
|
||||||
@@ -1288,6 +1307,15 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
|||||||
return GDK_EVENT_PROPAGATE;
|
return GDK_EVENT_PROPAGATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
modifier_state_opens_new_window(GtkWidget *widget, guint state)
|
||||||
|
{
|
||||||
|
GdkModifierType primary = gdk_keymap_get_modifier_mask(
|
||||||
|
gdk_keymap_get_for_display(gtk_widget_get_display(widget)),
|
||||||
|
GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
|
||||||
|
return state == primary || state == GDK_SHIFT_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
|
fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
|
||||||
{
|
{
|
||||||
@@ -1304,11 +1332,13 @@ fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
|
|||||||
if (!entry || entry != entry_at(self, event->x, event->y))
|
if (!entry || entry != entry_at(self, event->x, event->y))
|
||||||
return GDK_EVENT_PROPAGATE;
|
return GDK_EVENT_PROPAGATE;
|
||||||
|
|
||||||
|
|
||||||
guint state = event->state & gtk_accelerator_get_default_mod_mask();
|
guint state = event->state & gtk_accelerator_get_default_mod_mask();
|
||||||
if ((event->button == GDK_BUTTON_PRIMARY && state == 0))
|
if ((event->button == GDK_BUTTON_PRIMARY && state == 0))
|
||||||
return open_entry(widget, entry, FALSE);
|
return open_entry(widget, entry, FALSE);
|
||||||
if ((event->button == GDK_BUTTON_PRIMARY && state == GDK_CONTROL_MASK) ||
|
if ((event->button == GDK_BUTTON_MIDDLE && state == 0) ||
|
||||||
(event->button == GDK_BUTTON_MIDDLE && state == 0))
|
(event->button == GDK_BUTTON_PRIMARY &&
|
||||||
|
modifier_state_opens_new_window(widget, state)))
|
||||||
return open_entry(widget, entry, TRUE);
|
return open_entry(widget, entry, TRUE);
|
||||||
return GDK_EVENT_PROPAGATE;
|
return GDK_EVENT_PROPAGATE;
|
||||||
}
|
}
|
||||||
@@ -1559,9 +1589,18 @@ static gboolean
|
|||||||
fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
|
fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
|
||||||
{
|
{
|
||||||
FivBrowser *self = FIV_BROWSER(widget);
|
FivBrowser *self = FIV_BROWSER(widget);
|
||||||
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
|
guint state = event->state & gtk_accelerator_get_default_mod_mask();
|
||||||
|
switch (state) {
|
||||||
case 0:
|
case 0:
|
||||||
switch (event->keyval) {
|
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:
|
case GDK_KEY_Return:
|
||||||
if (self->selected)
|
if (self->selected)
|
||||||
return open_entry(widget, self->selected, FALSE);
|
return open_entry(widget, self->selected, FALSE);
|
||||||
@@ -1608,6 +1647,15 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modifier_state_opens_new_window(widget, state)) {
|
||||||
|
switch (event->keyval) {
|
||||||
|
case GDK_KEY_Return:
|
||||||
|
if (self->selected)
|
||||||
|
return open_entry(widget, self->selected, TRUE);
|
||||||
|
return GDK_EVENT_STOP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GTK_WIDGET_CLASS(fiv_browser_parent_class)
|
return GTK_WIDGET_CLASS(fiv_browser_parent_class)
|
||||||
->key_press_event(widget, event);
|
->key_press_event(widget, event);
|
||||||
}
|
}
|
||||||
@@ -1864,7 +1912,8 @@ fiv_browser_init(FivBrowser *self)
|
|||||||
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
|
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
|
||||||
for (size_t i = 0; i < self->thumbnailers_len; i++)
|
for (size_t i = 0; i < self->thumbnailers_len; i++)
|
||||||
self->thumbnailers[i].self = self;
|
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);
|
set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL);
|
||||||
self->show_labels = FALSE;
|
self->show_labels = FALSE;
|
||||||
@@ -1909,8 +1958,9 @@ on_model_reloaded(FivIoModel *model, FivBrowser *self)
|
|||||||
fiv_browser_select(self, selected_uri);
|
fiv_browser_select(self, selected_uri);
|
||||||
g_free(selected_uri);
|
g_free(selected_uri);
|
||||||
|
|
||||||
|
// Restarting thumbnailers is critical, because they keep Entry pointers.
|
||||||
reload_thumbnails(self);
|
reload_thumbnails(self);
|
||||||
thumbnailers_start(self);
|
thumbnailers_restart(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -1925,8 +1975,8 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
|
|||||||
g_ptr_array_add(self->entries, entry);
|
g_ptr_array_add(self->entries, entry);
|
||||||
|
|
||||||
reload_one_thumbnail(self, entry);
|
reload_one_thumbnail(self, entry);
|
||||||
// TODO(p): Try to add to thumbnailer queue if already started.
|
thumbnailers_enqueue(self, entry);
|
||||||
thumbnailers_start(self);
|
thumbnailers_deploy(self);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1952,8 +2002,9 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
|
|||||||
// so that there's no jumping around. Or, a bit more properly,
|
// so that there's no jumping around. Or, a bit more properly,
|
||||||
// move the thumbnail cache entry to the new URI.
|
// move the thumbnail cache entry to the new URI.
|
||||||
reload_one_thumbnail(self, found);
|
reload_one_thumbnail(self, found);
|
||||||
// TODO(p): Try to add to thumbnailer queue if already started.
|
// TODO(p): Rather cancel the entry in any running thumbnailer,
|
||||||
thumbnailers_start(self);
|
// remove it from queues, and _enqueue() + _deploy().
|
||||||
|
thumbnailers_restart(self);
|
||||||
} else {
|
} else {
|
||||||
found->removed = TRUE;
|
found->removed = TRUE;
|
||||||
gtk_widget_queue_draw(GTK_WIDGET(self));
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// fiv-context-menu.c: popup menu
|
// 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
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// 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)
|
if (bytes_in)
|
||||||
flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
|
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.
|
// TODO(p): Add a fallback to internal capabilities.
|
||||||
// The simplest is to specify the filename and the resolution.
|
// The simplest is to specify the filename and the resolution.
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
GSubprocess *subprocess = g_subprocess_new(flags, &error,
|
GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
|
||||||
#ifdef G_OS_WIN32
|
#ifdef G_OS_WIN32
|
||||||
"wperl",
|
"wperl",
|
||||||
#endif
|
#endif
|
||||||
"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
|
"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
|
||||||
"--binary", "-quiet", "--", path, NULL);
|
"--binary", "-quiet", "--", path, NULL);
|
||||||
|
g_object_unref(launcher);
|
||||||
if (error) {
|
if (error) {
|
||||||
info_redirect_error(dialog, error);
|
info_redirect_error(dialog, error);
|
||||||
return;
|
return;
|
||||||
@@ -328,17 +337,13 @@ open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
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 =
|
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_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
|
||||||
gtk_dialog_run(GTK_DIALOG(dialog));
|
gtk_dialog_run(GTK_DIALOG(dialog));
|
||||||
gtk_widget_destroy(dialog);
|
gtk_widget_destroy(dialog);
|
||||||
|
|
||||||
g_clear_object(&window);
|
|
||||||
g_error_free(error);
|
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(
|
(void) g_app_info_set_as_last_used_for_type(
|
||||||
self->app_info, self->content_type, NULL);
|
self->app_info, self->content_type, NULL);
|
||||||
} else {
|
} 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_list_free(files);
|
||||||
g_object_unref(context);
|
g_object_unref(context);
|
||||||
@@ -373,8 +380,11 @@ append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)
|
|||||||
ctx->app_info = opener;
|
ctx->app_info = opener;
|
||||||
|
|
||||||
// On Linux, this prefers the obsoleted X-GNOME-FullName.
|
// On Linux, this prefers the obsoleted X-GNOME-FullName.
|
||||||
gchar *name =
|
const char *display_name = g_app_info_get_display_name(opener);
|
||||||
g_strdup_printf("Open With %s", g_app_info_get_display_name(opener));
|
// Ironically, GIO reads CFBundleName and can't read CFBundleDisplayName.
|
||||||
|
if (!display_name)
|
||||||
|
display_name = g_app_info_get_executable(opener);
|
||||||
|
gchar *name = g_strdup_printf("Open With %s", display_name);
|
||||||
|
|
||||||
// It's documented that we can touch the child, if we want to use markup.
|
// It's documented that we can touch the child, if we want to use markup.
|
||||||
#if 0
|
#if 0
|
||||||
@@ -437,14 +447,22 @@ on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
|
|||||||
g_free(uri);
|
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
|
static void
|
||||||
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
|
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;
|
OpenContext *ctx = user_data;
|
||||||
GError *error = NULL;
|
GtkWindow *window = g_weak_ref_get(&ctx->window);
|
||||||
if (!g_file_trash(ctx->file, NULL, &error))
|
fiv_context_menu_remove(window, ctx->file);
|
||||||
open_context_show_error_dialog(ctx, error);
|
g_clear_object(&window);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
@@ -488,8 +506,6 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
|
|||||||
|
|
||||||
GAppInfo *default_ =
|
GAppInfo *default_ =
|
||||||
g_app_info_get_default_for_type(ctx->content_type, FALSE);
|
g_app_info_get_default_for_type(ctx->content_type, FALSE);
|
||||||
GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
|
|
||||||
GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
|
|
||||||
|
|
||||||
GtkWidget *menu = gtk_menu_new();
|
GtkWidget *menu = gtk_menu_new();
|
||||||
if (default_) {
|
if (default_) {
|
||||||
@@ -498,6 +514,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
|
|||||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
|
||||||
for (GList *iter = recommended; iter; iter = iter->next) {
|
for (GList *iter = recommended; iter; iter = iter->next) {
|
||||||
if (!default_ || !g_app_info_equal(iter->data, default_))
|
if (!default_ || !g_app_info_equal(iter->data, default_))
|
||||||
append_opener(menu, iter->data, ctx);
|
append_opener(menu, iter->data, ctx);
|
||||||
@@ -510,6 +527,10 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
|
|||||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The implementation returns the same data for both,
|
||||||
|
// we'd have to filter out the recommended ones from here.
|
||||||
|
#ifndef __APPLE__
|
||||||
|
GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
|
||||||
for (GList *iter = fallback; iter; iter = iter->next) {
|
for (GList *iter = fallback; iter; iter = iter->next) {
|
||||||
if (!default_ || !g_app_info_equal(iter->data, default_))
|
if (!default_ || !g_app_info_equal(iter->data, default_))
|
||||||
append_opener(menu, iter->data, ctx);
|
append_opener(menu, iter->data, ctx);
|
||||||
@@ -521,6 +542,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
|
|||||||
gtk_menu_shell_append(
|
gtk_menu_shell_append(
|
||||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
GtkWidget *item = gtk_menu_item_new_with_label("Open With...");
|
GtkWidget *item = gtk_menu_item_new_with_label("Open With...");
|
||||||
g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate),
|
g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// fiv-context-menu.h: popup menu
|
// 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
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@@ -18,4 +18,5 @@
|
|||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
|
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);
|
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);
|
||||||
|
|||||||
463
fiv-io-cmm.c
Normal file
463
fiv-io-cmm.c
Normal 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;
|
||||||
|
}
|
||||||
@@ -247,7 +247,9 @@ static GPtrArray *
|
|||||||
model_decide_placement(
|
model_decide_placement(
|
||||||
FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
|
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;
|
return NULL;
|
||||||
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
|
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
|
||||||
return subdirs;
|
return subdirs;
|
||||||
@@ -380,6 +382,9 @@ on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
|
|||||||
switch (event_type) {
|
switch (event_type) {
|
||||||
case G_FILE_MONITOR_EVENT_CHANGED:
|
case G_FILE_MONITOR_EVENT_CHANGED:
|
||||||
case G_FILE_MONITOR_EVENT_ATTRIBUTE_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;
|
event = MONITOR_CHANGING;
|
||||||
new_entry_file = file;
|
new_entry_file = file;
|
||||||
break;
|
break;
|
||||||
@@ -398,8 +403,6 @@ on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
|
|||||||
new_entry_file = file;
|
new_entry_file = file;
|
||||||
break;
|
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_PRE_UNMOUNT:
|
||||||
case G_FILE_MONITOR_EVENT_UNMOUNTED:
|
case G_FILE_MONITOR_EVENT_UNMOUNTED:
|
||||||
// TODO(p): Figure out how to handle _UNMOUNTED sensibly.
|
// TODO(p): Figure out how to handle _UNMOUNTED sensibly.
|
||||||
|
|||||||
452
fiv-io.c
452
fiv-io.c
@@ -34,11 +34,6 @@
|
|||||||
#include <libjpegqs.h>
|
#include <libjpegqs.h>
|
||||||
#endif // HAVE_JPEG_QS
|
#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
|
#define TIFF_TABLES_CONSTANTS_ONLY
|
||||||
#include "tiff-tables.h"
|
#include "tiff-tables.h"
|
||||||
#include "tiffer.h"
|
#include "tiffer.h"
|
||||||
@@ -47,6 +42,8 @@
|
|||||||
#include <libraw.h>
|
#include <libraw.h>
|
||||||
#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
|
#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
|
||||||
#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0
|
#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0
|
||||||
|
#else
|
||||||
|
#define rawparams params
|
||||||
#endif
|
#endif
|
||||||
#endif // HAVE_LIBRAW
|
#endif // HAVE_LIBRAW
|
||||||
#ifdef HAVE_RESVG
|
#ifdef HAVE_RESVG
|
||||||
@@ -64,6 +61,9 @@
|
|||||||
#ifdef HAVE_LIBTIFF
|
#ifdef HAVE_LIBTIFF
|
||||||
#include <tiff.h>
|
#include <tiff.h>
|
||||||
#include <tiffio.h>
|
#include <tiffio.h>
|
||||||
|
#ifndef TIFF_TMSIZE_T_MAX
|
||||||
|
#define TIFF_TMSIZE_T_MAX ((tmsize_t) (SIZE_MAX >> 1))
|
||||||
|
#endif
|
||||||
#endif // HAVE_LIBTIFF
|
#endif // HAVE_LIBTIFF
|
||||||
#ifdef HAVE_GDKPIXBUF
|
#ifdef HAVE_GDKPIXBUF
|
||||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||||
@@ -291,333 +291,6 @@ try_append_page(
|
|||||||
return true;
|
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_parametric(
|
|
||||||
double gamma, double whitepoint[2], double primaries[6])
|
|
||||||
{
|
|
||||||
#ifdef HAVE_LCMS2
|
|
||||||
// TODO(p): Make sure to use the library in a thread-safe manner.
|
|
||||||
cmsContext context = 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(context, gamma);
|
|
||||||
if (!curve)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
cmsHPROFILE profile = cmsCreateRGBProfileTHR(
|
|
||||||
context, &cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
|
|
||||||
cmsFreeToneCurve(curve);
|
|
||||||
return profile;
|
|
||||||
#else
|
|
||||||
(void) gamma;
|
|
||||||
(void) whitepoint;
|
|
||||||
(void) primaries;
|
|
||||||
return NULL;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
FivIoProfile
|
|
||||||
fiv_io_profile_new_sRGB_gamma(double gamma)
|
|
||||||
{
|
|
||||||
return fiv_io_profile_new_parametric(gamma,
|
|
||||||
(double[2]){0.3127, 0.3290},
|
|
||||||
(double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -------------------------------------------------------------------
|
// --- Wuffs -------------------------------------------------------------------
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@@ -711,8 +384,9 @@ struct load_wuffs_frame_context {
|
|||||||
GBytes *meta_iccp; ///< Reference-counted ICC profile
|
GBytes *meta_iccp; ///< Reference-counted ICC profile
|
||||||
GBytes *meta_xmp; ///< Reference-counted XMP
|
GBytes *meta_xmp; ///< Reference-counted XMP
|
||||||
|
|
||||||
FivIoProfile target; ///< Target device profile, if any
|
FivIoCmm *cmm; ///< CMM context, if any
|
||||||
FivIoProfile source; ///< Source colour profile, if any
|
FivIoProfile *target; ///< Target device profile, if any
|
||||||
|
FivIoProfile *source; ///< Source colour profile, if any
|
||||||
|
|
||||||
FivIoImage *result; ///< The resulting image (referenced)
|
FivIoImage *result; ///< The resulting image (referenced)
|
||||||
FivIoImage *result_tail; ///< The final animation frame
|
FivIoImage *result_tail; ///< The final animation frame
|
||||||
@@ -779,11 +453,12 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
|
|||||||
|
|
||||||
if (ctx->target) {
|
if (ctx->target) {
|
||||||
if (ctx->expand_16_float || ctx->pack_16_10) {
|
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);
|
targetbuf, ctx->width, ctx->height, ctx->source, ctx->target);
|
||||||
// The first one premultiplies below, the second doesn't need to.
|
// The first one premultiplies below, the second doesn't need to.
|
||||||
} else {
|
} else {
|
||||||
fiv_io_profile_argb32_premultiply(image, ctx->source, ctx->target);
|
fiv_io_cmm_argb32_premultiply(
|
||||||
|
ctx->cmm, image, ctx->source, ctx->target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,7 +596,8 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
|
|||||||
const FivIoOpenContext *ioctx, GError **error)
|
const FivIoOpenContext *ioctx, GError **error)
|
||||||
{
|
{
|
||||||
struct load_wuffs_frame_context ctx = {
|
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): PNG text chunks, like we do with PNG thumbnails.
|
||||||
// TODO(p): See if something could and should be done about
|
// TODO(p): See if something could and should be done about
|
||||||
@@ -1005,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.
|
// TODO(p): Improve our simplistic PNG handling of: gAMA, cHRM, sRGB.
|
||||||
if (ctx.target) {
|
if (ctx.target) {
|
||||||
if (ctx.meta_iccp)
|
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)
|
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.
|
// Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine.
|
||||||
@@ -1298,7 +976,7 @@ static uint32_t *
|
|||||||
parse_mpf_index_entries(const struct tiffer *T, struct tiffer_entry *entry)
|
parse_mpf_index_entries(const struct tiffer *T, struct tiffer_entry *entry)
|
||||||
{
|
{
|
||||||
uint32_t count = entry->remaining_count / 16;
|
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++) {
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
// 5.2.3.3.3. Individual Image Data Offset
|
// 5.2.3.3.3. Individual Image Data Offset
|
||||||
uint32_t offset = parse_mpf_mpentry(entry->p + i * 16, T);
|
uint32_t offset = parse_mpf_mpentry(entry->p + i * 16, T);
|
||||||
@@ -1390,8 +1068,8 @@ parse_exif_profile_subifd(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static FivIoProfile
|
static FivIoProfile *
|
||||||
parse_exif_profile(const void *data, size_t len)
|
parse_exif_profile(FivIoCmm *cmm, const void *data, size_t len)
|
||||||
{
|
{
|
||||||
struct tiffer T = {};
|
struct tiffer T = {};
|
||||||
if (!tiffer_init(&T, (const uint8_t *) data, len) || !tiffer_next_ifd(&T))
|
if (!tiffer_init(&T, (const uint8_t *) data, len) || !tiffer_next_ifd(&T))
|
||||||
@@ -1423,7 +1101,7 @@ parse_exif_profile(const void *data, size_t len)
|
|||||||
|
|
||||||
// If sRGB is claimed, assume all parameters are standard.
|
// If sRGB is claimed, assume all parameters are standard.
|
||||||
if (params.colorspace == Exif_ColorSpace_sRGB)
|
if (params.colorspace == Exif_ColorSpace_sRGB)
|
||||||
return fiv_io_profile_new_sRGB();
|
return fiv_io_cmm_get_profile_sRGB(cmm);
|
||||||
|
|
||||||
// AdobeRGB Nikon JPEGs provide all of these.
|
// AdobeRGB Nikon JPEGs provide all of these.
|
||||||
if (params.colorspace != Exif_ColorSpace_Uncalibrated ||
|
if (params.colorspace != Exif_ColorSpace_Uncalibrated ||
|
||||||
@@ -1432,7 +1110,7 @@ parse_exif_profile(const void *data, size_t len)
|
|||||||
!params.have_primaries)
|
!params.have_primaries)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
return fiv_io_profile_new_parametric(
|
return fiv_io_cmm_get_profile_parametric(cmm,
|
||||||
params.gamma, params.whitepoint, params.primaries);
|
params.gamma, params.whitepoint, params.primaries);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1586,18 +1264,18 @@ load_jpeg_finalize(FivIoImage *image, bool cmyk,
|
|||||||
else
|
else
|
||||||
g_byte_array_free(meta.icc, TRUE);
|
g_byte_array_free(meta.icc, TRUE);
|
||||||
|
|
||||||
FivIoProfile source = NULL;
|
FivIoProfile *source = NULL;
|
||||||
if (icc_profile)
|
if (icc_profile && ctx->cmm)
|
||||||
source = fiv_io_profile_new(
|
source = fiv_io_cmm_get_profile(ctx->cmm,
|
||||||
g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile));
|
g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile));
|
||||||
else if (image->exif)
|
else if (image->exif && ctx->cmm)
|
||||||
source = parse_exif_profile(
|
source = parse_exif_profile(ctx->cmm,
|
||||||
g_bytes_get_data(image->exif, NULL), g_bytes_get_size(image->exif));
|
g_bytes_get_data(image->exif, NULL), g_bytes_get_size(image->exif));
|
||||||
|
|
||||||
if (cmyk)
|
if (cmyk)
|
||||||
fiv_io_profile_cmyk(image, source, ctx->screen_profile);
|
fiv_io_cmm_cmyk(ctx->cmm, image, source, ctx->screen_profile);
|
||||||
else
|
else
|
||||||
fiv_io_profile_any(image, source, ctx->screen_profile);
|
fiv_io_cmm_any(ctx->cmm, image, source, ctx->screen_profile);
|
||||||
|
|
||||||
if (source)
|
if (source)
|
||||||
fiv_io_profile_free(source);
|
fiv_io_profile_free(source);
|
||||||
@@ -1865,9 +1543,11 @@ load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
|
|||||||
if (G_BYTE_ORDER == G_LITTLE_ENDIAN) {
|
if (G_BYTE_ORDER == G_LITTLE_ENDIAN) {
|
||||||
memcpy(dst, buf, area * sizeof *dst);
|
memcpy(dst, buf, area * sizeof *dst);
|
||||||
} else {
|
} else {
|
||||||
uint32_t *src = (uint32_t *) buf;
|
const uint32_t *src = (const uint32_t *) buf;
|
||||||
for (uint64_t i = 0; i < area; i++)
|
for (uint64_t i = 0; i < area; i++) {
|
||||||
*dst++ = GUINT32_FROM_LE(*src++);
|
uint32_t value = *src++;
|
||||||
|
*dst++ = GUINT32_FROM_LE(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// info->bgcolor is not reliable.
|
// info->bgcolor is not reliable.
|
||||||
@@ -1998,7 +1678,8 @@ open_libwebp(
|
|||||||
|
|
||||||
WebPDemuxDelete(demux);
|
WebPDemuxDelete(demux);
|
||||||
if (ctx->screen_profile)
|
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:
|
fail:
|
||||||
WebPFreeDecBuffer(&config.output);
|
WebPFreeDecBuffer(&config.output);
|
||||||
@@ -2380,7 +2061,7 @@ open_libraw(
|
|||||||
|
|
||||||
out:
|
out:
|
||||||
libraw_close(iprc);
|
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 ---------------------------------------------------------
|
#endif // HAVE_LIBRAW ---------------------------------------------------------
|
||||||
@@ -2402,8 +2083,8 @@ load_resvg_destroy(FivIoRenderClosure *closure)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static FivIoImage *
|
static FivIoImage *
|
||||||
load_resvg_render_internal(FivIoRenderClosureResvg *self,
|
load_resvg_render_internal(FivIoRenderClosureResvg *self, double scale,
|
||||||
double scale, FivIoProfile target, GError **error)
|
FivIoCmm *cmm, FivIoProfile *target, GError **error)
|
||||||
{
|
{
|
||||||
double w = ceil(self->width * scale), h = ceil(self->height * scale);
|
double w = ceil(self->width * scale), h = ceil(self->height * scale);
|
||||||
if (w > SHRT_MAX || h > SHRT_MAX) {
|
if (w > SHRT_MAX || h > SHRT_MAX) {
|
||||||
@@ -2433,15 +2114,15 @@ load_resvg_render_internal(FivIoRenderClosureResvg *self,
|
|||||||
uint32_t rgba = g_ntohl(pixels[i]);
|
uint32_t rgba = g_ntohl(pixels[i]);
|
||||||
pixels[i] = rgba << 24 | rgba >> 8;
|
pixels[i] = rgba << 24 | rgba >> 8;
|
||||||
}
|
}
|
||||||
return fiv_io_profile_finalize(image, target);
|
return fiv_io_cmm_finish(cmm, image, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
static FivIoImage *
|
static FivIoImage *
|
||||||
load_resvg_render(
|
load_resvg_render(FivIoRenderClosure *closure,
|
||||||
FivIoRenderClosure *closure, FivIoProfile target, double scale)
|
FivIoCmm *cmm, FivIoProfile *target, double scale)
|
||||||
{
|
{
|
||||||
FivIoRenderClosureResvg *self = (FivIoRenderClosureResvg *) closure;
|
FivIoRenderClosureResvg *self = (FivIoRenderClosureResvg *) closure;
|
||||||
return load_resvg_render_internal(self, scale, target, NULL);
|
return load_resvg_render_internal(self, scale, cmm, target, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *
|
static const char *
|
||||||
@@ -2499,8 +2180,8 @@ open_resvg(
|
|||||||
closure->width = size.width;
|
closure->width = size.width;
|
||||||
closure->height = size.height;
|
closure->height = size.height;
|
||||||
|
|
||||||
FivIoImage *image =
|
FivIoImage *image = load_resvg_render_internal(
|
||||||
load_resvg_render_internal(closure, 1., ctx->screen_profile, error);
|
closure, 1., ctx->cmm, ctx->screen_profile, error);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
load_resvg_destroy(&closure->parent);
|
load_resvg_destroy(&closure->parent);
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -2530,7 +2211,7 @@ load_librsvg_destroy(FivIoRenderClosure *closure)
|
|||||||
|
|
||||||
static FivIoImage *
|
static FivIoImage *
|
||||||
load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
|
load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
|
||||||
FivIoProfile target, GError **error)
|
FivIoCmm *cmm, FivIoProfile *target, GError **error)
|
||||||
{
|
{
|
||||||
RsvgRectangle viewport = {.x = 0, .y = 0,
|
RsvgRectangle viewport = {.x = 0, .y = 0,
|
||||||
.width = self->width * scale, .height = self->height * scale};
|
.width = self->width * scale, .height = self->height * scale};
|
||||||
@@ -2557,15 +2238,15 @@ load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
|
|||||||
fiv_io_image_unref(image);
|
fiv_io_image_unref(image);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
return fiv_io_profile_finalize(image, target);
|
return fiv_io_cmm_finish(cmm, image, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
static FivIoImage *
|
static FivIoImage *
|
||||||
load_librsvg_render(
|
load_librsvg_render(FivIoRenderClosure *closure,
|
||||||
FivIoRenderClosure *closure, FivIoProfile target, double scale)
|
FivIoCmm *cmm, FivIoProfile *target, double scale)
|
||||||
{
|
{
|
||||||
FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure;
|
FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure;
|
||||||
return load_librsvg_render_internal(self, scale, target, NULL);
|
return load_librsvg_render_internal(self, scale, cmm, target, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static FivIoImage *
|
static FivIoImage *
|
||||||
@@ -2614,8 +2295,8 @@ open_librsvg(
|
|||||||
|
|
||||||
// librsvg rasterizes filters, so rendering to a recording Cairo surface
|
// librsvg rasterizes filters, so rendering to a recording Cairo surface
|
||||||
// has been abandoned.
|
// has been abandoned.
|
||||||
FivIoImage *image =
|
FivIoImage *image = load_librsvg_render_internal(
|
||||||
load_librsvg_render_internal(closure, 1., ctx->screen_profile, error);
|
closure, 1., ctx->cmm, ctx->screen_profile, error);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
load_librsvg_destroy(&closure->parent);
|
load_librsvg_destroy(&closure->parent);
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -2931,7 +2612,7 @@ open_libheif(
|
|||||||
g_free(ids);
|
g_free(ids);
|
||||||
fail_read:
|
fail_read:
|
||||||
heif_context_free(ctx);
|
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 --------------------------------------------------------
|
#endif // HAVE_LIBHEIF --------------------------------------------------------
|
||||||
@@ -3162,7 +2843,7 @@ fail:
|
|||||||
TIFFSetWarningHandlerExt(whe);
|
TIFFSetWarningHandlerExt(whe);
|
||||||
TIFFSetErrorHandler(eh);
|
TIFFSetErrorHandler(eh);
|
||||||
TIFFSetWarningHandler(wh);
|
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 --------------------------------------------------------
|
#endif // HAVE_LIBTIFF --------------------------------------------------------
|
||||||
@@ -3257,9 +2938,10 @@ open_gdkpixbuf(
|
|||||||
|
|
||||||
g_object_unref(pixbuf);
|
g_object_unref(pixbuf);
|
||||||
if (custom_argb32)
|
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
|
else
|
||||||
image = fiv_io_profile_finalize(image, ctx->screen_profile);
|
image = fiv_io_cmm_finish(ctx->cmm, image, ctx->screen_profile);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3712,7 +3394,7 @@ set_metadata(WebPMux *mux, const char *fourcc, GBytes *data)
|
|||||||
}
|
}
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile target,
|
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile *target,
|
||||||
const char *path, GError **error)
|
const char *path, GError **error)
|
||||||
{
|
{
|
||||||
g_return_val_if_fail(page != NULL, FALSE);
|
g_return_val_if_fail(page != NULL, FALSE);
|
||||||
@@ -3776,34 +3458,40 @@ fiv_io_orientation_apply(const FivIoImage *image,
|
|||||||
FivIoOrientation orientation, double *width, double *height)
|
FivIoOrientation orientation, double *width, double *height)
|
||||||
{
|
{
|
||||||
fiv_io_orientation_dimensions(image, orientation, width, 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_t matrix = {};
|
||||||
cairo_matrix_init_identity(&matrix);
|
cairo_matrix_init_identity(&matrix);
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case FivIoOrientation90:
|
case FivIoOrientation90:
|
||||||
cairo_matrix_rotate(&matrix, -M_PI_2);
|
cairo_matrix_rotate(&matrix, -M_PI_2);
|
||||||
cairo_matrix_translate(&matrix, -*width, 0);
|
cairo_matrix_translate(&matrix, -width, 0);
|
||||||
break;
|
break;
|
||||||
case FivIoOrientation180:
|
case FivIoOrientation180:
|
||||||
cairo_matrix_scale(&matrix, -1, -1);
|
cairo_matrix_scale(&matrix, -1, -1);
|
||||||
cairo_matrix_translate(&matrix, -*width, -*height);
|
cairo_matrix_translate(&matrix, -width, -height);
|
||||||
break;
|
break;
|
||||||
case FivIoOrientation270:
|
case FivIoOrientation270:
|
||||||
cairo_matrix_rotate(&matrix, +M_PI_2);
|
cairo_matrix_rotate(&matrix, +M_PI_2);
|
||||||
cairo_matrix_translate(&matrix, 0, -*height);
|
cairo_matrix_translate(&matrix, 0, -height);
|
||||||
break;
|
break;
|
||||||
case FivIoOrientationMirror0:
|
case FivIoOrientationMirror0:
|
||||||
cairo_matrix_scale(&matrix, -1, +1);
|
cairo_matrix_scale(&matrix, -1, +1);
|
||||||
cairo_matrix_translate(&matrix, -*width, 0);
|
cairo_matrix_translate(&matrix, -width, 0);
|
||||||
break;
|
break;
|
||||||
case FivIoOrientationMirror90:
|
case FivIoOrientationMirror90:
|
||||||
cairo_matrix_rotate(&matrix, +M_PI_2);
|
cairo_matrix_rotate(&matrix, +M_PI_2);
|
||||||
cairo_matrix_scale(&matrix, -1, +1);
|
cairo_matrix_scale(&matrix, -1, +1);
|
||||||
cairo_matrix_translate(&matrix, -*width, -*height);
|
cairo_matrix_translate(&matrix, -width, -height);
|
||||||
break;
|
break;
|
||||||
case FivIoOrientationMirror180:
|
case FivIoOrientationMirror180:
|
||||||
cairo_matrix_scale(&matrix, +1, -1);
|
cairo_matrix_scale(&matrix, +1, -1);
|
||||||
cairo_matrix_translate(&matrix, 0, -*height);
|
cairo_matrix_translate(&matrix, 0, -height);
|
||||||
break;
|
break;
|
||||||
case FivIoOrientationMirror270:
|
case FivIoOrientationMirror270:
|
||||||
cairo_matrix_rotate(&matrix, -M_PI_2);
|
cairo_matrix_rotate(&matrix, -M_PI_2);
|
||||||
|
|||||||
67
fiv-io.h
67
fiv-io.h
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// fiv-io.h: image operations
|
// 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
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@@ -22,16 +22,53 @@
|
|||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <webp/encode.h> // WebPConfig
|
#include <webp/encode.h> // WebPConfig
|
||||||
|
|
||||||
|
typedef enum _FivIoOrientation FivIoOrientation;
|
||||||
|
typedef struct _FivIoRenderClosure FivIoRenderClosure;
|
||||||
|
typedef struct _FivIoImage FivIoImage;
|
||||||
|
typedef struct _FivIoProfile FivIoProfile;
|
||||||
|
|
||||||
// --- Colour management -------------------------------------------------------
|
// --- 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.
|
GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
|
||||||
typedef void *FivIoProfile;
|
void fiv_io_profile_free(FivIoProfile *self);
|
||||||
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);
|
|
||||||
|
|
||||||
// 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 -----------------------------------------------------------------
|
// --- Loading -----------------------------------------------------------------
|
||||||
|
|
||||||
@@ -39,10 +76,6 @@ extern const char *fiv_io_supported_media_types[];
|
|||||||
|
|
||||||
gchar **fiv_io_all_supported_media_types(void);
|
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
|
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
|
||||||
enum _FivIoOrientation {
|
enum _FivIoOrientation {
|
||||||
FivIoOrientationUnknown = 0,
|
FivIoOrientationUnknown = 0,
|
||||||
@@ -60,7 +93,8 @@ enum _FivIoOrientation {
|
|||||||
// then loaders could store it in their closures.
|
// then loaders could store it in their closures.
|
||||||
struct _FivIoRenderClosure {
|
struct _FivIoRenderClosure {
|
||||||
/// The rendering is allowed to fail, returning NULL.
|
/// The rendering is allowed to fail, returning NULL.
|
||||||
FivIoImage *(*render)(FivIoRenderClosure *, FivIoProfile, double scale);
|
FivIoImage *(*render)(
|
||||||
|
FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
|
||||||
void (*destroy)(FivIoRenderClosure *);
|
void (*destroy)(FivIoRenderClosure *);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,7 +165,8 @@ cairo_surface_t *fiv_io_image_to_surface_noref(const FivIoImage *image);
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char *uri; ///< Source URI
|
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
|
int screen_dpi; ///< Target DPI
|
||||||
gboolean enhance; ///< Enhance JPEG (currently)
|
gboolean enhance; ///< Enhance JPEG (currently)
|
||||||
gboolean first_frame_only; ///< Only interested in the 1st frame
|
gboolean first_frame_only; ///< Only interested in the 1st frame
|
||||||
@@ -150,6 +185,8 @@ FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error);
|
|||||||
/// and its target dimensions.
|
/// and its target dimensions.
|
||||||
cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image,
|
cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image,
|
||||||
FivIoOrientation orientation, double *width, double *height);
|
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,
|
void fiv_io_orientation_dimensions(const FivIoImage *image,
|
||||||
FivIoOrientation orientation, double *width, double *height);
|
FivIoOrientation orientation, double *width, double *height);
|
||||||
|
|
||||||
@@ -179,4 +216,4 @@ unsigned char *fiv_io_encode_webp(
|
|||||||
/// Saves the page as a lossless WebP still picture or animation.
|
/// Saves the page as a lossless WebP still picture or animation.
|
||||||
/// If no exact frame is specified, this potentially creates an animation.
|
/// If no exact frame is specified, this potentially creates an animation.
|
||||||
gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
|
gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
|
||||||
FivIoProfile target, const char *path, GError **error);
|
FivIoProfile *target, const char *path, GError **error);
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ if [ "$#" -ne 2 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
|
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
|
||||||
| curl --silent --show-error --upload-file - https://transfer.sh/image \
|
| curl --silent --show-error --form 'files[]=@-' https://uguu.se/upload \
|
||||||
| jq --slurp --raw-input --raw-output @uri)"
|
| jq --raw-output '.files[] | .url | @uri')"
|
||||||
|
|||||||
@@ -433,7 +433,10 @@ complete_path(GFile *location, GtkListStore *model)
|
|||||||
!info)
|
!info)
|
||||||
break;
|
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))
|
g_file_info_get_is_hidden(info))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -534,6 +537,13 @@ on_show_enter_location(
|
|||||||
g_signal_connect(entry, "changed",
|
g_signal_connect(entry, "changed",
|
||||||
G_CALLBACK(on_enter_location_changed), self);
|
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.
|
// Can't have it ellipsized and word-wrapped at the same time.
|
||||||
GtkWidget *protocols = gtk_label_new("");
|
GtkWidget *protocols = gtk_label_new("");
|
||||||
gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);
|
gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);
|
||||||
|
|||||||
@@ -137,10 +137,12 @@ might_be_a_thumbnail(const char *path_or_uri)
|
|||||||
static FivIoImage *
|
static FivIoImage *
|
||||||
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
|
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
|
||||||
{
|
{
|
||||||
|
FivIoCmm *cmm = fiv_io_cmm_get_default();
|
||||||
FivIoOpenContext ctx = {
|
FivIoOpenContext ctx = {
|
||||||
.uri = g_file_get_uri(target),
|
.uri = g_file_get_uri(target),
|
||||||
// Remember to synchronize changes with adjust_thumbnail().
|
// Remember to synchronize changes with adjust_thumbnail().
|
||||||
.screen_profile = fiv_io_profile_new_sRGB(),
|
.cmm = cmm,
|
||||||
|
.screen_profile = fiv_io_cmm_get_profile_sRGB(cmm),
|
||||||
.screen_dpi = 96,
|
.screen_dpi = 96,
|
||||||
.first_frame_only = TRUE,
|
.first_frame_only = TRUE,
|
||||||
// Only using this array as a redirect.
|
// Only using this array as a redirect.
|
||||||
@@ -182,9 +184,11 @@ adjust_thumbnail(FivIoImage *thumbnail, double row_height)
|
|||||||
FivIoRenderClosure *closure = thumbnail->render;
|
FivIoRenderClosure *closure = thumbnail->render;
|
||||||
if (closure && orientation <= FivIoOrientation0) {
|
if (closure && orientation <= FivIoOrientation0) {
|
||||||
// Remember to synchronize changes with render().
|
// Remember to synchronize changes with render().
|
||||||
FivIoProfile screen_profile = fiv_io_profile_new_sRGB();
|
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.
|
// This API doesn't accept non-uniform scaling; prefer a vertical fit.
|
||||||
FivIoImage *scaled = closure->render(closure, screen_profile, scale_y);
|
FivIoImage *scaled =
|
||||||
|
closure->render(closure, cmm, screen_profile, scale_y);
|
||||||
if (screen_profile)
|
if (screen_profile)
|
||||||
fiv_io_profile_free(screen_profile);
|
fiv_io_profile_free(screen_profile);
|
||||||
if (scaled)
|
if (scaled)
|
||||||
@@ -388,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.:
|
// 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_Outdoor_.IIQ
|
||||||
// - Phase One/H 25/H25_IT8.7-2_Card.TIF
|
// - Phase One/H 25/H25_IT8.7-2_Card.TIF
|
||||||
*flip = iprc->sizes.flip
|
*flip = iprc->sizes.flip;
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
560
fiv-view.c
560
fiv-view.c
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// fiv-view.c: image viewing widget
|
// fiv-view.c: image viewing widget
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021 - 2022, 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
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include <epoxy/gl.h>
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
#ifdef GDK_WINDOWING_X11
|
#ifdef GDK_WINDOWING_X11
|
||||||
#include <gdk/gdkx.h>
|
#include <gdk/gdkx.h>
|
||||||
@@ -78,11 +79,15 @@ struct _FivView {
|
|||||||
double drag_start[2]; ///< Adjustment values for drag origin
|
double drag_start[2]; ///< Adjustment values for drag origin
|
||||||
|
|
||||||
FivIoImage *enhance_swap; ///< Quick swap in/out
|
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
|
int remaining_loops; ///< Greater than zero if limited
|
||||||
gint64 frame_time; ///< Current frame's start, µs precision
|
gint64 frame_time; ///< Current frame's start, µs precision
|
||||||
gulong frame_update_connection; ///< GdkFrameClock::update
|
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,
|
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.
|
// Globals are, sadly, the canonical way of storing signal numbers.
|
||||||
static guint view_signals[LAST_SIGNAL];
|
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
|
static void
|
||||||
on_adjustment_value_changed(
|
on_adjustment_value_changed(
|
||||||
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
|
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
|
||||||
@@ -417,6 +563,7 @@ prescale_page(FivView *self)
|
|||||||
// If it fails, the previous frame pointer may become invalid.
|
// If it fails, the previous frame pointer may become invalid.
|
||||||
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
|
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
|
||||||
self->frame = self->page_scaled = closure->render(closure,
|
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);
|
self->enable_cms ? self->screen_cms_profile : NULL, self->scale);
|
||||||
if (!self->page_scaled)
|
if (!self->page_scaled)
|
||||||
self->frame = self->page;
|
self->frame = self->page;
|
||||||
@@ -461,7 +608,7 @@ out:
|
|||||||
//
|
//
|
||||||
// Note that Wayland does not have any appropriate protocol, as of writing:
|
// Note that Wayland does not have any appropriate protocol, as of writing:
|
||||||
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
|
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
|
||||||
static FivIoProfile
|
static FivIoProfile *
|
||||||
monitor_cms_profile(GdkWindow *root, int num)
|
monitor_cms_profile(GdkWindow *root, int num)
|
||||||
{
|
{
|
||||||
char atom[32] = "";
|
char atom[32] = "";
|
||||||
@@ -471,11 +618,12 @@ monitor_cms_profile(GdkWindow *root, int num)
|
|||||||
int format = 0, length = 0;
|
int format = 0, length = 0;
|
||||||
GdkAtom type = GDK_NONE;
|
GdkAtom type = GDK_NONE;
|
||||||
guchar *data = NULL;
|
guchar *data = NULL;
|
||||||
FivIoProfile result = NULL;
|
FivIoProfile *result = NULL;
|
||||||
if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
|
if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
|
||||||
8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
|
8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
|
||||||
if (format == 8 && length > 0)
|
if (format == 8 && length > 0)
|
||||||
result = fiv_io_profile_new(data, length);
|
result = fiv_io_cmm_get_profile(
|
||||||
|
fiv_io_cmm_get_default(), data, length);
|
||||||
g_free(data);
|
g_free(data);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -498,7 +646,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
|
|||||||
gchar *data = NULL;
|
gchar *data = NULL;
|
||||||
gsize length = 0;
|
gsize length = 0;
|
||||||
if (g_file_get_contents(path, &data, &length, NULL))
|
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(data);
|
||||||
}
|
}
|
||||||
g_free(path);
|
g_free(path);
|
||||||
@@ -525,7 +674,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
|
|||||||
|
|
||||||
out:
|
out:
|
||||||
if (!self->screen_cms_profile)
|
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
|
static void
|
||||||
@@ -559,6 +709,9 @@ fiv_view_realize(GtkWidget *widget)
|
|||||||
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
|
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
|
||||||
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
|
&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",
|
// Without the following call, or the rendering mode set to "recording",
|
||||||
// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
|
// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
|
||||||
// creates backing stores using cairo_content_t constants.
|
// creates backing stores using cairo_content_t constants.
|
||||||
@@ -568,20 +721,274 @@ fiv_view_realize(GtkWidget *widget)
|
|||||||
// Note that this disables double buffering, and sometimes causes artefacts,
|
// Note that this disables double buffering, and sometimes causes artefacts,
|
||||||
// see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
|
// see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
|
||||||
//
|
//
|
||||||
// If GTK+'s OpenGL integration fails to deliver, we need to use the window
|
// GTK+'s OpenGL integration is terrible, so we may need to use
|
||||||
// directly, sidestepping the toolkit entirely.
|
// the X11 subwindow directly, sidestepping the toolkit entirely.
|
||||||
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
|
|
||||||
if (GDK_IS_X11_WINDOW(window) &&
|
if (GDK_IS_X11_WINDOW(window) &&
|
||||||
g_settings_get_boolean(settings, "native-view-window"))
|
g_settings_get_boolean(settings, "native-view-window"))
|
||||||
gdk_window_ensure_native(window);
|
gdk_window_ensure_native(window);
|
||||||
g_object_unref(settings);
|
|
||||||
#endif // GDK_WINDOWING_X11
|
#endif // GDK_WINDOWING_X11
|
||||||
|
g_object_unref(settings);
|
||||||
|
|
||||||
gtk_widget_register_window(widget, window);
|
gtk_widget_register_window(widget, window);
|
||||||
gtk_widget_set_window(widget, window);
|
gtk_widget_set_window(widget, window);
|
||||||
gtk_widget_set_realized(widget, TRUE);
|
gtk_widget_set_realized(widget, TRUE);
|
||||||
|
|
||||||
reload_screen_cms_profile(FIV_VIEW(widget), window);
|
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
|
static gboolean
|
||||||
@@ -598,8 +1005,10 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
|
|||||||
if (!self->image ||
|
if (!self->image ||
|
||||||
!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
|
!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
|
||||||
return TRUE;
|
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);
|
get_display_dimensions(self, &dw, &dh);
|
||||||
|
|
||||||
double x = 0;
|
double x = 0;
|
||||||
@@ -1029,6 +1438,127 @@ get_toplevel(GtkWidget *widget)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct zoom_ask_context {
|
||||||
|
GtkWidget *result_left, *result_right;
|
||||||
|
Dimensions dimensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_zoom_ask_spin_changed(GtkSpinButton *spin, G_GNUC_UNUSED gpointer user_data)
|
||||||
|
{
|
||||||
|
// We don't want to call gtk_spin_button_update(),
|
||||||
|
// that would immediately replace whatever the user has typed in.
|
||||||
|
gdouble scale = -1;
|
||||||
|
const gchar *text = gtk_entry_get_text(GTK_ENTRY(spin));
|
||||||
|
if (*text) {
|
||||||
|
gchar *end = NULL;
|
||||||
|
gdouble value = g_strtod(text, &end);
|
||||||
|
if (!*end)
|
||||||
|
scale = value / 100.;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct zoom_ask_context *data = user_data;
|
||||||
|
GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(spin));
|
||||||
|
if (scale <= 0) {
|
||||||
|
gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING);
|
||||||
|
gtk_label_set_text(GTK_LABEL(data->result_left), "—");
|
||||||
|
gtk_label_set_text(GTK_LABEL(data->result_right), "—");
|
||||||
|
} else {
|
||||||
|
gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING);
|
||||||
|
gchar *left = g_strdup_printf("%.0f", data->dimensions.width * scale);
|
||||||
|
gchar *right = g_strdup_printf("%.0f", data->dimensions.height * scale);
|
||||||
|
gtk_label_set_text(GTK_LABEL(data->result_left), left);
|
||||||
|
gtk_label_set_text(GTK_LABEL(data->result_right), right);
|
||||||
|
g_free(left);
|
||||||
|
g_free(right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
zoom_ask(FivView *self)
|
||||||
|
{
|
||||||
|
GtkWidget *dialog = gtk_dialog_new_with_buttons("Set zoom level",
|
||||||
|
get_toplevel(GTK_WIDGET(self)),
|
||||||
|
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL |
|
||||||
|
GTK_DIALOG_USE_HEADER_BAR,
|
||||||
|
"_OK", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
|
||||||
|
|
||||||
|
Dimensions dimensions = get_surface_dimensions(self);
|
||||||
|
gchar *original_width = g_strdup_printf("%.0f", dimensions.width);
|
||||||
|
gchar *original_height = g_strdup_printf("%.0f", dimensions.height);
|
||||||
|
GtkWidget *original_left = gtk_label_new(original_width);
|
||||||
|
GtkWidget *original_middle = gtk_label_new("×");
|
||||||
|
GtkWidget *original_right = gtk_label_new(original_height);
|
||||||
|
g_free(original_width);
|
||||||
|
g_free(original_height);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(original_left), 1.);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(original_right), 0.);
|
||||||
|
|
||||||
|
GtkWidget *original_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
gtk_box_pack_start(
|
||||||
|
GTK_BOX(original_box), original_left, TRUE, TRUE, 0);
|
||||||
|
gtk_box_pack_start(
|
||||||
|
GTK_BOX(original_box), original_middle, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(
|
||||||
|
GTK_BOX(original_box), original_right, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
// FIXME: This widget's behaviour is absolutely miserable.
|
||||||
|
// For example, we would like to be flexible with decimal spaces.
|
||||||
|
GtkAdjustment *adjustment = gtk_adjustment_new(
|
||||||
|
self->scale * 100, 0., 100000., 1., 10., 0.);
|
||||||
|
GtkWidget *spin = gtk_spin_button_new(adjustment, 1., 2);
|
||||||
|
gtk_spin_button_set_update_policy(
|
||||||
|
GTK_SPIN_BUTTON(spin), GTK_UPDATE_IF_VALID);
|
||||||
|
gtk_entry_set_activates_default(GTK_ENTRY(spin), TRUE);
|
||||||
|
|
||||||
|
GtkWidget *zoom_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
GtkWidget *zoom_label = gtk_label_new_with_mnemonic("_Zoom:");
|
||||||
|
gtk_label_set_mnemonic_widget(GTK_LABEL(zoom_label), spin);
|
||||||
|
gtk_box_pack_start(GTK_BOX(zoom_box), zoom_label, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(zoom_box), spin, TRUE, TRUE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(zoom_box), gtk_label_new("%"), FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget *result_left = gtk_label_new(NULL);
|
||||||
|
GtkWidget *result_middle = gtk_label_new("×");
|
||||||
|
GtkWidget *result_right = gtk_label_new(NULL);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(result_left), 1.);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(result_right), 0.);
|
||||||
|
|
||||||
|
GtkSizeGroup *group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
|
||||||
|
gtk_size_group_add_widget(group, original_left);
|
||||||
|
gtk_size_group_add_widget(group, original_right);
|
||||||
|
gtk_size_group_add_widget(group, result_left);
|
||||||
|
gtk_size_group_add_widget(group, result_right);
|
||||||
|
|
||||||
|
GtkWidget *result_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
gtk_box_pack_start(GTK_BOX(result_box), result_left, TRUE, TRUE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(result_box), result_middle, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(result_box), result_right, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
struct zoom_ask_context data = { result_left, result_right, dimensions };
|
||||||
|
g_signal_connect(spin, "changed",
|
||||||
|
G_CALLBACK(on_zoom_ask_spin_changed), &data);
|
||||||
|
on_zoom_ask_spin_changed(GTK_SPIN_BUTTON(spin), &data);
|
||||||
|
|
||||||
|
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||||
|
g_object_set(content, "margin", 12, NULL);
|
||||||
|
gtk_box_set_spacing(GTK_BOX(content), 6);
|
||||||
|
gtk_container_add(GTK_CONTAINER(content), original_box);
|
||||||
|
gtk_container_add(GTK_CONTAINER(content), zoom_box);
|
||||||
|
gtk_container_add(GTK_CONTAINER(content), result_box);
|
||||||
|
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
|
||||||
|
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
|
||||||
|
gtk_widget_show_all(dialog);
|
||||||
|
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||||||
|
double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));
|
||||||
|
if (value > 0)
|
||||||
|
set_scale(self, value / 100., NULL);
|
||||||
|
}
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
g_object_unref(group);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
copy(FivView *self)
|
copy(FivView *self)
|
||||||
{
|
{
|
||||||
@@ -1117,7 +1647,7 @@ static gboolean
|
|||||||
save_as(FivView *self, FivIoImage *frame)
|
save_as(FivView *self, FivIoImage *frame)
|
||||||
{
|
{
|
||||||
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
|
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
|
||||||
FivIoProfile target = NULL;
|
FivIoProfile *target = NULL;
|
||||||
if (self->enable_cms && (target = self->screen_cms_profile)) {
|
if (self->enable_cms && (target = self->screen_cms_profile)) {
|
||||||
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
|
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
|
||||||
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
|
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
|
||||||
@@ -1293,6 +1823,7 @@ fiv_view_class_init(FivViewClass *klass)
|
|||||||
widget_class->map = fiv_view_map;
|
widget_class->map = fiv_view_map;
|
||||||
widget_class->unmap = fiv_view_unmap;
|
widget_class->unmap = fiv_view_unmap;
|
||||||
widget_class->realize = fiv_view_realize;
|
widget_class->realize = fiv_view_realize;
|
||||||
|
widget_class->unrealize = fiv_view_unrealize;
|
||||||
widget_class->draw = fiv_view_draw;
|
widget_class->draw = fiv_view_draw;
|
||||||
widget_class->button_press_event = fiv_view_button_press_event;
|
widget_class->button_press_event = fiv_view_button_press_event;
|
||||||
widget_class->scroll_event = fiv_view_scroll_event;
|
widget_class->scroll_event = fiv_view_scroll_event;
|
||||||
@@ -1381,6 +1912,7 @@ open_without_swapping_in(FivView *self, const char *uri)
|
|||||||
{
|
{
|
||||||
FivIoOpenContext ctx = {
|
FivIoOpenContext ctx = {
|
||||||
.uri = uri,
|
.uri = uri,
|
||||||
|
.cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL,
|
||||||
.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
|
.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
|
||||||
.screen_dpi = 96, // TODO(p): Try to retrieve it from the screen.
|
.screen_dpi = 96, // TODO(p): Try to retrieve it from the screen.
|
||||||
.enhance = self->enhance,
|
.enhance = self->enhance,
|
||||||
@@ -1617,6 +2149,8 @@ fiv_view_command(FivView *self, FivViewCommand command)
|
|||||||
set_scale(self, self->scale / SCALE_STEP, NULL);
|
set_scale(self, self->scale / SCALE_STEP, NULL);
|
||||||
break; case FIV_VIEW_COMMAND_ZOOM_1:
|
break; case FIV_VIEW_COMMAND_ZOOM_1:
|
||||||
set_scale(self, 1.0, NULL);
|
set_scale(self, 1.0, NULL);
|
||||||
|
break; case FIV_VIEW_COMMAND_ZOOM_ASK:
|
||||||
|
zoom_ask(self);
|
||||||
break; case FIV_VIEW_COMMAND_FIT_WIDTH:
|
break; case FIV_VIEW_COMMAND_FIT_WIDTH:
|
||||||
set_scale_to_fit_width(self);
|
set_scale_to_fit_width(self);
|
||||||
break; case FIV_VIEW_COMMAND_FIT_HEIGHT:
|
break; case FIV_VIEW_COMMAND_FIT_HEIGHT:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ typedef enum _FivViewCommand {
|
|||||||
XX(FIV_VIEW_COMMAND_ZOOM_IN, "zoom-in") \
|
XX(FIV_VIEW_COMMAND_ZOOM_IN, "zoom-in") \
|
||||||
XX(FIV_VIEW_COMMAND_ZOOM_OUT, "zoom-out") \
|
XX(FIV_VIEW_COMMAND_ZOOM_OUT, "zoom-out") \
|
||||||
XX(FIV_VIEW_COMMAND_ZOOM_1, "zoom-1") \
|
XX(FIV_VIEW_COMMAND_ZOOM_1, "zoom-1") \
|
||||||
|
XX(FIV_VIEW_COMMAND_ZOOM_ASK, "zoom-ask") \
|
||||||
XX(FIV_VIEW_COMMAND_FIT_WIDTH, "fit-width") \
|
XX(FIV_VIEW_COMMAND_FIT_WIDTH, "fit-width") \
|
||||||
XX(FIV_VIEW_COMMAND_FIT_HEIGHT, "fit-height") \
|
XX(FIV_VIEW_COMMAND_FIT_HEIGHT, "fit-height") \
|
||||||
XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit") \
|
XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit") \
|
||||||
|
|||||||
220
fiv.c
220
fiv.c
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
|
// 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 - 2025, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@@ -44,11 +44,6 @@
|
|||||||
#include "fiv-thumbnail.h"
|
#include "fiv-thumbnail.h"
|
||||||
#include "fiv-view.h"
|
#include "fiv-view.h"
|
||||||
|
|
||||||
#ifdef HAVE_LCMS2_FAST_FLOAT
|
|
||||||
#include <lcms2.h>
|
|
||||||
#include <lcms2_fast_float.h>
|
|
||||||
#endif // HAVE_LCMS2_FAST_FLOAT
|
|
||||||
|
|
||||||
// --- Utilities ---------------------------------------------------------------
|
// --- Utilities ---------------------------------------------------------------
|
||||||
|
|
||||||
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
|
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
|
||||||
@@ -78,6 +73,138 @@ slist_to_strv(GSList *slist)
|
|||||||
return strv;
|
return strv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- macOS utilities ---------------------------------------------------------
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
|
||||||
|
static gchar *
|
||||||
|
cfurlref_to_path(CFURLRef urlref)
|
||||||
|
{
|
||||||
|
CFStringRef path = CFURLCopyFileSystemPath(urlref, kCFURLPOSIXPathStyle);
|
||||||
|
if (!path)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
CFIndex size = CFStringGetMaximumSizeForEncoding(
|
||||||
|
CFStringGetLength(path), kCFStringEncodingUTF8) + 1;
|
||||||
|
gchar *string = g_malloc(size);
|
||||||
|
|
||||||
|
Boolean ok = CFStringGetCString(path, string, size, kCFStringEncodingUTF8);
|
||||||
|
CFRelease(path);
|
||||||
|
if (!ok) {
|
||||||
|
g_free(string);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gchar *
|
||||||
|
get_application_bundle_path(void)
|
||||||
|
{
|
||||||
|
gchar *result = NULL;
|
||||||
|
CFBundleRef bundle = CFBundleGetMainBundle();
|
||||||
|
if (!bundle)
|
||||||
|
goto fail_1;
|
||||||
|
|
||||||
|
// When launched from outside a bundle, it will make up one,
|
||||||
|
// but these paths will then be equal.
|
||||||
|
CFURLRef bundle_url = CFBundleCopyBundleURL(bundle);
|
||||||
|
if (!bundle_url)
|
||||||
|
goto fail_1;
|
||||||
|
CFURLRef resources_url = CFBundleCopyResourcesDirectoryURL(bundle);
|
||||||
|
if (!resources_url)
|
||||||
|
goto fail_2;
|
||||||
|
|
||||||
|
if (!CFEqual(bundle_url, resources_url))
|
||||||
|
result = cfurlref_to_path(bundle_url);
|
||||||
|
|
||||||
|
CFRelease(resources_url);
|
||||||
|
fail_2:
|
||||||
|
CFRelease(bundle_url);
|
||||||
|
fail_1:
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gchar *
|
||||||
|
prepend_path_string(const gchar *prepended, const gchar *original)
|
||||||
|
{
|
||||||
|
if (!prepended)
|
||||||
|
return g_strdup(original ? original : "");
|
||||||
|
if (!original || !*original)
|
||||||
|
return g_strdup(prepended);
|
||||||
|
|
||||||
|
GHashTable *seen = g_hash_table_new(g_str_hash, g_str_equal);
|
||||||
|
GPtrArray *unique = g_ptr_array_new();
|
||||||
|
g_ptr_array_add(unique, (gpointer) prepended);
|
||||||
|
g_hash_table_add(seen, (gpointer) prepended);
|
||||||
|
|
||||||
|
gchar **components = g_strsplit(original, ":", -1);
|
||||||
|
for (gchar **p = components; *p; p++) {
|
||||||
|
if (g_hash_table_contains(seen, *p))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
g_ptr_array_add(unique, *p);
|
||||||
|
g_hash_table_add(seen, *p);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_ptr_array_add(unique, NULL);
|
||||||
|
gchar *result = g_strjoinv(":", (gchar **) unique->pdata);
|
||||||
|
g_hash_table_destroy(seen);
|
||||||
|
g_ptr_array_free(unique, TRUE);
|
||||||
|
|
||||||
|
g_strfreev(components);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We reuse foreign dependencies, so we need to prevent them from loading
|
||||||
|
// any system-wide files, and point them in the right direction.
|
||||||
|
static void
|
||||||
|
adjust_environment(void)
|
||||||
|
{
|
||||||
|
gchar *bundle_dir = get_application_bundle_path();
|
||||||
|
if (!bundle_dir)
|
||||||
|
return;
|
||||||
|
|
||||||
|
gchar *contents_dir = g_build_filename(bundle_dir, "Contents", NULL);
|
||||||
|
gchar *macos_dir = g_build_filename(contents_dir, "MacOS", NULL);
|
||||||
|
gchar *resources_dir = g_build_filename(contents_dir, "Resources", NULL);
|
||||||
|
gchar *datadir = g_build_filename(resources_dir, "share", NULL);
|
||||||
|
gchar *libdir = g_build_filename(resources_dir, "lib", NULL);
|
||||||
|
g_free(bundle_dir);
|
||||||
|
|
||||||
|
gchar *new_path = prepend_path_string(macos_dir, g_getenv("PATH"));
|
||||||
|
g_setenv("PATH", new_path, TRUE);
|
||||||
|
g_free(new_path);
|
||||||
|
|
||||||
|
const gchar *data_dirs = g_getenv("XDG_DATA_DIRS");
|
||||||
|
gchar *new_data_dirs = data_dirs && *data_dirs
|
||||||
|
? prepend_path_string(datadir, data_dirs)
|
||||||
|
: prepend_path_string(datadir, "/usr/local/share:/usr/share");
|
||||||
|
g_setenv("XDG_DATA_DIRS", new_data_dirs, TRUE);
|
||||||
|
g_free(new_data_dirs);
|
||||||
|
|
||||||
|
gchar *schemas_dir = g_build_filename(datadir, "glib-2.0", "schemas", NULL);
|
||||||
|
g_setenv("GSETTINGS_SCHEMA_DIR", schemas_dir, TRUE);
|
||||||
|
g_free(schemas_dir);
|
||||||
|
|
||||||
|
gchar *gdk_pixbuf_module_file =
|
||||||
|
g_build_filename(libdir, "gdk-pixbuf-2.0", "loaders.cache", NULL);
|
||||||
|
g_setenv("GDK_PIXBUF_MODULE_FILE", gdk_pixbuf_module_file, TRUE);
|
||||||
|
g_free(gdk_pixbuf_module_file);
|
||||||
|
|
||||||
|
// GTK+ is smart enough to also consider application bundles,
|
||||||
|
// but let there be a single source of truth.
|
||||||
|
g_setenv("GTK_EXE_PREFIX", resources_dir, TRUE);
|
||||||
|
|
||||||
|
g_free(libdir);
|
||||||
|
g_free(datadir);
|
||||||
|
g_free(resources_dir);
|
||||||
|
g_free(macos_dir);
|
||||||
|
g_free(contents_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
// --- Keyboard shortcuts ------------------------------------------------------
|
// --- Keyboard shortcuts ------------------------------------------------------
|
||||||
// Fuck XML, this can be easily represented in static structures.
|
// Fuck XML, this can be easily represented in static structures.
|
||||||
// Though it would be nice if the accelerators could be customized.
|
// Though it would be nice if the accelerators could be customized.
|
||||||
@@ -672,7 +799,7 @@ enum {
|
|||||||
XX(S3, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
|
XX(S3, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
|
||||||
XX(FIXATE, T("pin2-symbolic", "Keep zoom and position")) \
|
XX(FIXATE, T("pin2-symbolic", "Keep zoom and position")) \
|
||||||
XX(MINUS, B("zoom-out-symbolic", "Zoom out")) \
|
XX(MINUS, B("zoom-out-symbolic", "Zoom out")) \
|
||||||
XX(SCALE, gtk_label_new("")) \
|
XX(SCALE, B(NULL, "Set zoom level")) \
|
||||||
XX(PLUS, B("zoom-in-symbolic", "Zoom in")) \
|
XX(PLUS, B("zoom-in-symbolic", "Zoom in")) \
|
||||||
XX(ONE, B("zoom-original-symbolic", "Original size")) \
|
XX(ONE, B("zoom-original-symbolic", "Original size")) \
|
||||||
XX(FIT, T("zoom-fit-best-symbolic", "Scale to fit")) \
|
XX(FIT, T("zoom-fit-best-symbolic", "Scale to fit")) \
|
||||||
@@ -1097,9 +1224,22 @@ on_next(void)
|
|||||||
static gchar **
|
static gchar **
|
||||||
build_spawn_argv(const char *uri)
|
build_spawn_argv(const char *uri)
|
||||||
{
|
{
|
||||||
// Because we only pass URIs, there is no need to prepend "--" here.
|
|
||||||
GPtrArray *a = g_ptr_array_new();
|
GPtrArray *a = g_ptr_array_new();
|
||||||
g_ptr_array_add(a, g_strdup(PROJECT_NAME));
|
#ifdef __APPLE__
|
||||||
|
// Otherwise we would always launch ourselves in the background.
|
||||||
|
gchar *bundle_dir = get_application_bundle_path();
|
||||||
|
if (bundle_dir) {
|
||||||
|
g_ptr_array_add(a, g_strdup("open"));
|
||||||
|
g_ptr_array_add(a, g_strdup("-a"));
|
||||||
|
g_ptr_array_add(a, bundle_dir);
|
||||||
|
// At least with G_APPLICATION_NON_UNIQUE, this is necessary:
|
||||||
|
g_ptr_array_add(a, g_strdup("-n"));
|
||||||
|
g_ptr_array_add(a, g_strdup("--args"));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// Because we only pass URIs, there is no need to prepend "--" after this.
|
||||||
|
if (!a->len)
|
||||||
|
g_ptr_array_add(a, g_strdup(PROJECT_NAME));
|
||||||
|
|
||||||
// Process-local VFS URIs need to be resolved to globally accessible URIs.
|
// Process-local VFS URIs need to be resolved to globally accessible URIs.
|
||||||
// It doesn't seem possible to reliably tell if a GFile is process-local,
|
// It doesn't seem possible to reliably tell if a GFile is process-local,
|
||||||
@@ -1408,15 +1548,24 @@ on_window_state_event(G_GNUC_UNUSED GtkWidget *widget,
|
|||||||
static void
|
static void
|
||||||
show_help_contents(void)
|
show_help_contents(void)
|
||||||
{
|
{
|
||||||
gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME);
|
|
||||||
#ifdef G_OS_WIN32
|
#ifdef G_OS_WIN32
|
||||||
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
|
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
|
||||||
|
#elif defined __APPLE__
|
||||||
|
gchar *prefix = get_application_bundle_path();
|
||||||
|
if (!prefix) {
|
||||||
|
show_error_dialog(g_error_new(
|
||||||
|
G_FILE_ERROR, G_FILE_ERROR_FAILED, "Cannot locate bundle"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
gchar *prefix = g_strdup(PROJECT_PREFIX);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME);
|
||||||
gchar *path = g_build_filename(prefix, PROJECT_DOCDIR, filename, NULL);
|
gchar *path = g_build_filename(prefix, PROJECT_DOCDIR, filename, NULL);
|
||||||
g_free(prefix);
|
g_free(prefix);
|
||||||
#else
|
|
||||||
gchar *path = g_build_filename(PROJECT_DOCDIR, filename, NULL);
|
|
||||||
#endif
|
|
||||||
g_free(filename);
|
g_free(filename);
|
||||||
|
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
gchar *uri = g_filename_to_uri(path, NULL, &error);
|
gchar *uri = g_filename_to_uri(path, NULL, &error);
|
||||||
g_free(path);
|
g_free(path);
|
||||||
@@ -1666,8 +1815,10 @@ static GtkWidget *
|
|||||||
make_toolbar_button(const char *symbolic, const char *tooltip)
|
make_toolbar_button(const char *symbolic, const char *tooltip)
|
||||||
{
|
{
|
||||||
GtkWidget *button = gtk_button_new();
|
GtkWidget *button = gtk_button_new();
|
||||||
gtk_button_set_image(GTK_BUTTON(button),
|
if (symbolic) {
|
||||||
gtk_image_new_from_icon_name(symbolic, GTK_ICON_SIZE_BUTTON));
|
gtk_button_set_image(GTK_BUTTON(button),
|
||||||
|
gtk_image_new_from_icon_name(symbolic, GTK_ICON_SIZE_BUTTON));
|
||||||
|
}
|
||||||
gtk_widget_set_tooltip_text(button, tooltip);
|
gtk_widget_set_tooltip_text(button, tooltip);
|
||||||
gtk_widget_set_focus_on_click(button, FALSE);
|
gtk_widget_set_focus_on_click(button, FALSE);
|
||||||
gtk_style_context_add_class(
|
gtk_style_context_add_class(
|
||||||
@@ -1694,6 +1845,9 @@ make_toolbar_radio(const char *label, const char *tooltip)
|
|||||||
GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
|
GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
|
||||||
gtk_widget_set_tooltip_text(button, tooltip);
|
gtk_widget_set_tooltip_text(button, tooltip);
|
||||||
gtk_widget_set_focus_on_click(button, FALSE);
|
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;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,7 +1909,6 @@ make_browser_toolbar(void)
|
|||||||
gtk_radio_button_join_group(radio, last);
|
gtk_radio_button_join_group(radio, last);
|
||||||
last = radio;
|
last = radio;
|
||||||
}
|
}
|
||||||
|
|
||||||
return browser_toolbar;
|
return browser_toolbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1811,7 +1964,8 @@ on_notify_view_scale(
|
|||||||
g_object_get(object, g_param_spec_get_name(param_spec), &scale, NULL);
|
g_object_get(object, g_param_spec_get_name(param_spec), &scale, NULL);
|
||||||
|
|
||||||
gchar *scale_str = g_strdup_printf("%.0f%%", round(scale * 100));
|
gchar *scale_str = g_strdup_printf("%.0f%%", round(scale * 100));
|
||||||
gtk_label_set_text(GTK_LABEL(g.toolbar[TOOLBAR_SCALE]), scale_str);
|
gtk_label_set_text(GTK_LABEL(
|
||||||
|
gtk_bin_get_child(GTK_BIN(g.toolbar[TOOLBAR_SCALE]))), scale_str);
|
||||||
g_free(scale_str);
|
g_free(scale_str);
|
||||||
|
|
||||||
// FIXME: The label doesn't immediately assume its new width.
|
// FIXME: The label doesn't immediately assume its new width.
|
||||||
@@ -1896,13 +2050,11 @@ make_view_toolbar(void)
|
|||||||
TOOLBAR(XX)
|
TOOLBAR(XX)
|
||||||
#undef XX
|
#undef XX
|
||||||
|
|
||||||
gtk_widget_set_margin_start(g.toolbar[TOOLBAR_SCALE], 5);
|
GtkWidget *scale_label = gtk_label_new("");
|
||||||
gtk_widget_set_margin_end(g.toolbar[TOOLBAR_SCALE], 5);
|
gtk_container_add(GTK_CONTAINER(g.toolbar[TOOLBAR_SCALE]), scale_label);
|
||||||
|
|
||||||
// So that the width doesn't jump around in the usual zoom range.
|
// So that the width doesn't jump around in the usual zoom range.
|
||||||
// Ideally, we'd measure the widest digit and use width(NNN%).
|
// Ideally, we'd measure the widest digit and use width(NNN%).
|
||||||
gtk_label_set_width_chars(GTK_LABEL(g.toolbar[TOOLBAR_SCALE]), 5);
|
gtk_label_set_width_chars(GTK_LABEL(scale_label), 5);
|
||||||
gtk_widget_set_halign(g.toolbar[TOOLBAR_SCALE], GTK_ALIGN_CENTER);
|
|
||||||
|
|
||||||
// GtkStatusBar solves a problem we do not have here.
|
// GtkStatusBar solves a problem we do not have here.
|
||||||
GtkWidget *view_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
GtkWidget *view_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||||
@@ -1933,6 +2085,7 @@ make_view_toolbar(void)
|
|||||||
toolbar_command(TOOLBAR_PLAY_PAUSE, FIV_VIEW_COMMAND_TOGGLE_PLAYBACK);
|
toolbar_command(TOOLBAR_PLAY_PAUSE, FIV_VIEW_COMMAND_TOGGLE_PLAYBACK);
|
||||||
toolbar_command(TOOLBAR_SEEK_FORWARD, FIV_VIEW_COMMAND_FRAME_NEXT);
|
toolbar_command(TOOLBAR_SEEK_FORWARD, FIV_VIEW_COMMAND_FRAME_NEXT);
|
||||||
toolbar_command(TOOLBAR_MINUS, FIV_VIEW_COMMAND_ZOOM_OUT);
|
toolbar_command(TOOLBAR_MINUS, FIV_VIEW_COMMAND_ZOOM_OUT);
|
||||||
|
toolbar_command(TOOLBAR_SCALE, FIV_VIEW_COMMAND_ZOOM_ASK);
|
||||||
toolbar_command(TOOLBAR_PLUS, FIV_VIEW_COMMAND_ZOOM_IN);
|
toolbar_command(TOOLBAR_PLUS, FIV_VIEW_COMMAND_ZOOM_IN);
|
||||||
toolbar_command(TOOLBAR_ONE, FIV_VIEW_COMMAND_ZOOM_1);
|
toolbar_command(TOOLBAR_ONE, FIV_VIEW_COMMAND_ZOOM_1);
|
||||||
toolbar_toggler(TOOLBAR_FIT, "scale-to-fit");
|
toolbar_toggler(TOOLBAR_FIT, "scale-to-fit");
|
||||||
@@ -2211,7 +2364,10 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
|
|||||||
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
|
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
|
||||||
fiv-view, fiv-browser { background: @content_view_bg; } \
|
fiv-view, fiv-browser { background: @content_view_bg; } \
|
||||||
placessidebar.fiv box > separator { margin: 4px 0; } \
|
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 { 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:first-child { padding-left: 4px; } \
|
||||||
.fiv-toolbar > button:last-child { padding-right: 4px; } \
|
.fiv-toolbar > button:last-child { padding-right: 4px; } \
|
||||||
.fiv-toolbar separator { \
|
.fiv-toolbar separator { \
|
||||||
@@ -2452,7 +2608,7 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static struct {
|
static struct {
|
||||||
gboolean browse, extract_thumbnail;
|
gboolean browse, collection, extract_thumbnail;
|
||||||
gchar **args, *thumbnail_size, *thumbnail_size_search;
|
gchar **args, *thumbnail_size, *thumbnail_size_search;
|
||||||
} o;
|
} o;
|
||||||
|
|
||||||
@@ -2462,12 +2618,12 @@ on_app_activate(
|
|||||||
{
|
{
|
||||||
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
|
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
|
||||||
// interpret multiple command line arguments differently, as a collection.
|
// interpret multiple command line arguments differently, as a collection.
|
||||||
// However, single-element collections are unrepresentable this way.
|
// However, single-element collections are unrepresentable this way,
|
||||||
// Should we allow multiple targets only in a special new mode?
|
// so we have a switch to enforce it.
|
||||||
g.files_index = -1;
|
g.files_index = -1;
|
||||||
if (o.args) {
|
if (o.args) {
|
||||||
const gchar *target = *o.args;
|
const gchar *target = *o.args;
|
||||||
if (o.args[1]) {
|
if (o.args[1] || o.collection) {
|
||||||
fiv_collection_reload(o.args);
|
fiv_collection_reload(o.args);
|
||||||
target = FIV_COLLECTION_SCHEME ":/";
|
target = FIV_COLLECTION_SCHEME ":/";
|
||||||
}
|
}
|
||||||
@@ -2583,11 +2739,6 @@ on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
|
|||||||
return 0;
|
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.
|
// Normalize all arguments to URIs, and run thumbnailing modes first.
|
||||||
for (gsize i = 0; o.args && o.args[i]; i++) {
|
for (gsize i = 0; o.args && o.args[i]; i++) {
|
||||||
GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
|
GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
|
||||||
@@ -2618,6 +2769,9 @@ main(int argc, char *argv[])
|
|||||||
{"browse", 0, G_OPTION_FLAG_IN_MAIN,
|
{"browse", 0, G_OPTION_FLAG_IN_MAIN,
|
||||||
G_OPTION_ARG_NONE, &o.browse,
|
G_OPTION_ARG_NONE, &o.browse,
|
||||||
"Start in filesystem browsing mode", NULL},
|
"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,
|
{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
|
||||||
G_OPTION_ARG_NONE, NULL,
|
G_OPTION_ARG_NONE, NULL,
|
||||||
"Invalidate the wide thumbnail cache", NULL},
|
"Invalidate the wide thumbnail cache", NULL},
|
||||||
@@ -2642,6 +2796,10 @@ main(int argc, char *argv[])
|
|||||||
{},
|
{},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
adjust_environment();
|
||||||
|
#endif
|
||||||
|
|
||||||
// We never get the ::open signal, thanks to G_OPTION_ARG_FILENAME_ARRAY.
|
// We never get the ::open signal, thanks to G_OPTION_ARG_FILENAME_ARRAY.
|
||||||
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
|
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
|
||||||
g_application_set_option_context_parameter_string(
|
g_application_set_option_context_parameter_string(
|
||||||
|
|||||||
@@ -17,6 +17,13 @@
|
|||||||
double buffering.
|
double buffering.
|
||||||
</description>
|
</description>
|
||||||
</key>
|
</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'>
|
<key name='dark-theme' type='b'>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
<summary>Use a dark theme variant on start-up</summary>
|
<summary>Use a dark theme variant on start-up</summary>
|
||||||
|
|||||||
52
macos-Info.plist.in
Normal file
52
macos-Info.plist.in
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>@ProjectName@</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>@ProjectNS@@ProjectName@</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>@ProjectName@</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>@ProjectName@.icns</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>@ProjectVersion@</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
|
||||||
|
<!-- Although mostly static, this should eventually be generated. -->
|
||||||
|
<!-- In particular, we should expand image/x-dcraw, -->
|
||||||
|
<!-- using information we can collect from shared-mime-info. -->
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Image File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Default</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.apple.icns</string>
|
||||||
|
<string>com.apple.quicktime-image</string>
|
||||||
|
<string>com.compuserve.gif</string>
|
||||||
|
<string>com.microsoft.bmp</string>
|
||||||
|
<string>com.microsoft.ico</string>
|
||||||
|
<string>org.webmproject.webp</string>
|
||||||
|
<string>public.avif</string>
|
||||||
|
<string>public.heic</string>
|
||||||
|
<string>public.heif</string>
|
||||||
|
<string>public.jpeg</string>
|
||||||
|
<string>public.png</string>
|
||||||
|
<string>public.svg-image</string>
|
||||||
|
<string>public.tiff</string>
|
||||||
|
<string>public.xbitmap-image</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
25
macos-configure.sh
Executable file
25
macos-configure.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
# macos-configure.sh: set up a Homebrew-based macOS build
|
||||||
|
#
|
||||||
|
# Meson has no special support for macOS application bundles whatsoever.
|
||||||
|
#
|
||||||
|
# gtk-mac-bundler doesn't do anything particularly miraculous,
|
||||||
|
# and it doesn't directly support Homebrew.
|
||||||
|
#
|
||||||
|
# It would be cleaner and more reproducible to set up a special HOMEBREW_PREFIX,
|
||||||
|
# though right now we're happy to build an app bundle at all.
|
||||||
|
#
|
||||||
|
# It would also allow us to make a custom Little CMS build that includes
|
||||||
|
# the fast float plugin, which is a bit of a big deal.
|
||||||
|
|
||||||
|
# TODO: exiftool (Perl is part of macOS, at least for now)
|
||||||
|
HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_ASK=1 brew install
|
||||||
|
coreutils meson pkgconf shared-mime-info adwaita-icon-theme \
|
||||||
|
gtk+3 jpeg-xl libavif libheif libraw librsvg little-cms2 webp
|
||||||
|
|
||||||
|
sourcedir=$(grealpath "${2:-$(dirname "$0")}")
|
||||||
|
builddir=$(grealpath "${1:-builddir}")
|
||||||
|
appdir=$builddir/fiv.app
|
||||||
|
meson setup --buildtype=debugoptimized --prefix="$appdir" \
|
||||||
|
--bindir=Contents/MacOS --libdir=Contents/Resources/lib \
|
||||||
|
--datadir=Contents/Resources/share "$builddir" "$sourcedir"
|
||||||
115
macos-install.sh
Executable file
115
macos-install.sh
Executable file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
export LC_ALL=C
|
||||||
|
cd "$MESON_INSTALL_DESTDIR_PREFIX"
|
||||||
|
|
||||||
|
# Input: Half-baked application bundle linked against Homebrew.
|
||||||
|
# Output: Portable application bundle.
|
||||||
|
source=/opt/homebrew
|
||||||
|
bindir=Contents/MacOS
|
||||||
|
libdir=Contents/Resources/lib
|
||||||
|
datadir=Contents/Resources/share
|
||||||
|
|
||||||
|
mkdir -p "$datadir"/glib-2.0/schemas
|
||||||
|
cp -p "$source"/share/glib-2.0/schemas/org.gtk.Settings.* \
|
||||||
|
"$datadir"/glib-2.0/schemas
|
||||||
|
mkdir -p "$datadir"/icons
|
||||||
|
cp -pRL "$source"/share/icons/Adwaita "$datadir/"icons
|
||||||
|
mkdir -p "$datadir"/icons/hicolor
|
||||||
|
cp -p "$source"/share/icons/hicolor/index.theme "$datadir"/icons/hicolor
|
||||||
|
mkdir -p "$datadir/mime"
|
||||||
|
# GIO doesn't use the database on macOS, this subset is for us.
|
||||||
|
find "$source"/share/mime/ -maxdepth 1 -type f -exec cp -p {} "$datadir"/mime \;
|
||||||
|
|
||||||
|
# Copy binaries we directly or indirectly depend on.
|
||||||
|
#
|
||||||
|
# Homebrew is a bit chaotic in that some libraries are linked against locations
|
||||||
|
# in /opt/homebrew/Cellar, and some against /opt/homebrew/opt symlinks.
|
||||||
|
# We'll process things in such a way that it does not matter.
|
||||||
|
#
|
||||||
|
# As a side note, libraries in /usr/lib are now actually being served from
|
||||||
|
# a shared cache by the dynamic linker and aren't visible on the filesystem.
|
||||||
|
# There is an alternative to "otool -L" which can see them but it isn't
|
||||||
|
# particularly nicer to parse: "dyld_info -dependents/-linked_dylibs".
|
||||||
|
rm -rf "$libdir"
|
||||||
|
mkdir -p "$libdir"
|
||||||
|
|
||||||
|
pixbufdir=$libdir/gdk-pixbuf-2.0
|
||||||
|
loadersdir=$pixbufdir/loaders
|
||||||
|
cp -RL "$source"/lib/gdk-pixbuf-2.0/* "$pixbufdir"
|
||||||
|
|
||||||
|
# Fix a piece of crap loader that needs to be special.
|
||||||
|
svg=$loadersdir/libpixbufloader_svg.so
|
||||||
|
rm -f "$loadersdir"/libpixbufloader_svg.dylib
|
||||||
|
otool -L "$svg" | grep -o '@rpath/[^ ]*' | while IFS= read -r bad
|
||||||
|
do install_name_tool -change "$bad" "$source/lib/$(basename "$bad")" "$svg"
|
||||||
|
done
|
||||||
|
|
||||||
|
GDK_PIXBUF_MODULEDIR=$loadersdir gdk-pixbuf-query-loaders \
|
||||||
|
| sed "s,$libdir,@rpath," > "$pixbufdir/loaders.cache"
|
||||||
|
|
||||||
|
gtkdir=$libdir/gtk-3.0
|
||||||
|
printbackendsdir=$gtkdir/printbackends
|
||||||
|
cp -RL "$source"/lib/gtk-3.0/* "$gtkdir"
|
||||||
|
|
||||||
|
# TODO: Figure out how to make gtk-query-immodules-3.0 pick up exactly
|
||||||
|
# what it needs to. So far I'm not sure if this is at all even useful.
|
||||||
|
rm -rf "$gtkdir"/immodules*
|
||||||
|
|
||||||
|
find "$bindir" "$loadersdir" "$printbackendsdir" -type f -maxdepth 1 | awk '
|
||||||
|
function collect(binary, command, line) {
|
||||||
|
if (seen[binary]++)
|
||||||
|
return
|
||||||
|
|
||||||
|
command = "otool -L \"" binary "\""
|
||||||
|
while ((command | getline line) > 0)
|
||||||
|
if (match(line, /^\t\/opt\/.+ \(/))
|
||||||
|
collect(substr(line, RSTART + 1, RLENGTH - 3))
|
||||||
|
close(command)
|
||||||
|
} {
|
||||||
|
collect($0)
|
||||||
|
delete seen[$0]
|
||||||
|
} END {
|
||||||
|
for (library in seen)
|
||||||
|
print library
|
||||||
|
}
|
||||||
|
' | while IFS= read -r binary
|
||||||
|
do test -f "$libdir/$(basename "$binary")" || cp "$binary" "$libdir"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Now redirect all binaries to internal linking.
|
||||||
|
# A good overview of how this works is "man dyld" and:
|
||||||
|
# https://itwenty.me/posts/01-understanding-rpath/
|
||||||
|
rewrite() {
|
||||||
|
otool -L "$1" | sed -n 's,^\t\(.*\) (.*,\1,p' | grep '^/opt/' \
|
||||||
|
| while IFS= read -r lib
|
||||||
|
do install_name_tool -change "$lib" "@rpath/$(basename "$lib")" "$1"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
find "$bindir" -type f -maxdepth 1 | while IFS= read -r binary
|
||||||
|
do
|
||||||
|
install_name_tool -add_rpath @executable_path/../Resources/lib "$binary"
|
||||||
|
rewrite "$binary"
|
||||||
|
done
|
||||||
|
|
||||||
|
find "$libdir" -type f \( -name '*.so' -o -name '*.dylib' \) \
|
||||||
|
| while IFS= read -r binary
|
||||||
|
do
|
||||||
|
chmod 644 "$binary"
|
||||||
|
install_name_tool -id "@rpath/${binary#$libdir/}" "$binary"
|
||||||
|
rewrite "$binary"
|
||||||
|
|
||||||
|
# Discard pointless @loader_path/../lib and absolute Homebrew paths.
|
||||||
|
otool -l "$binary" | awk '
|
||||||
|
$1 == "cmd" { command = $2 }
|
||||||
|
command == "LC_RPATH" && $1 == "path" { print $2 }
|
||||||
|
' | xargs -R 1 -I % install_name_tool -delete_rpath % "$binary"
|
||||||
|
|
||||||
|
# Replace freshly invalidated code signatures with ad-hoc ones.
|
||||||
|
codesign --force --sign - "$binary"
|
||||||
|
done
|
||||||
|
|
||||||
|
glib-compile-schemas "$datadir"/glib-2.0/schemas
|
||||||
|
|
||||||
|
# This may speed up program start-up a little bit.
|
||||||
|
gtk-update-icon-cache "$datadir"/icons/Adwaita
|
||||||
22
macos-svg2icns.sh
Executable file
22
macos-svg2icns.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
# macos-svg2icns.sh: convert an SVG to the macOS .icns format
|
||||||
|
if [ $# -ne 2 ]
|
||||||
|
then
|
||||||
|
echo >&2 "Usage: $0 INPUT.svg OUTPUT.icns"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
svg=$1 icns=$2 tmpdir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
|
||||||
|
iconset="$tmpdir/$(basename "$icns" .icns).iconset"
|
||||||
|
mkdir -p "$iconset"
|
||||||
|
for size in 16 32 128 256 512
|
||||||
|
do
|
||||||
|
size2x=$((size * 2))
|
||||||
|
rsvg-convert --output="$iconset/icon_${size}x${size}.png" \
|
||||||
|
--width=$size --height=$size "$svg"
|
||||||
|
rsvg-convert --output="$iconset/icon_${size}x${size}@2x.png" \
|
||||||
|
--width=$size2x --height=$size2x "$svg"
|
||||||
|
done
|
||||||
|
iconutil -c icns -o "$icns" "$iconset"
|
||||||
66
meson.build
66
meson.build
@@ -1,7 +1,7 @@
|
|||||||
# vim: noet ts=4 sts=4 sw=4:
|
# vim: noet ts=4 sts=4 sw=4:
|
||||||
project('fiv', 'c',
|
project('fiv', 'c',
|
||||||
default_options : ['c_std=gnu99', 'warning_level=2'],
|
default_options : ['c_std=gnu99', 'warning_level=2'],
|
||||||
version : '0.1.0',
|
version : '1.0.0',
|
||||||
meson_version : '>=0.57')
|
meson_version : '>=0.57')
|
||||||
|
|
||||||
cc = meson.get_compiler('c')
|
cc = meson.get_compiler('c')
|
||||||
@@ -18,6 +18,8 @@ add_project_arguments(
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
win32 = host_machine.system() == 'windows'
|
win32 = host_machine.system() == 'windows'
|
||||||
|
macos = host_machine.system() == 'darwin' \
|
||||||
|
and host_machine.subsystem() == 'macos'
|
||||||
|
|
||||||
# The likelihood of this being installed is nearly zero. Enable the wrap.
|
# The likelihood of this being installed is nearly zero. Enable the wrap.
|
||||||
libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
|
libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
|
||||||
@@ -36,6 +38,7 @@ gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
dependency('gtk+-3.0'),
|
dependency('gtk+-3.0'),
|
||||||
dependency('pixman-1'),
|
dependency('pixman-1'),
|
||||||
|
dependency('epoxy'),
|
||||||
|
|
||||||
dependency('libjpeg'),
|
dependency('libjpeg'),
|
||||||
dependency('libturbojpeg'),
|
dependency('libturbojpeg'),
|
||||||
@@ -96,15 +99,20 @@ docdir = get_option('datadir') / 'doc' / meson.project_name()
|
|||||||
application_ns = 'name.janouch.'
|
application_ns = 'name.janouch.'
|
||||||
application_url = 'https://janouch.name/p/' + meson.project_name()
|
application_url = 'https://janouch.name/p/' + meson.project_name()
|
||||||
|
|
||||||
|
rawconf = configuration_data({
|
||||||
|
'ProjectName' : meson.project_name(),
|
||||||
|
'ProjectVersion' : meson.project_version(),
|
||||||
|
'ProjectNS' : application_ns,
|
||||||
|
'ProjectURL' : application_url,
|
||||||
|
})
|
||||||
|
|
||||||
conf = configuration_data()
|
conf = configuration_data()
|
||||||
conf.set_quoted('PROJECT_NAME', meson.project_name())
|
conf.set_quoted('PROJECT_NAME', meson.project_name())
|
||||||
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
|
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
|
||||||
conf.set_quoted('PROJECT_NS', application_ns)
|
conf.set_quoted('PROJECT_NS', application_ns)
|
||||||
conf.set_quoted('PROJECT_URL', application_url)
|
conf.set_quoted('PROJECT_URL', application_url)
|
||||||
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
|
conf.set_quoted('PROJECT_PREFIX', get_option('prefix'))
|
||||||
if win32
|
conf.set_quoted('PROJECT_DOCDIR', docdir)
|
||||||
conf.set_quoted('PROJECT_DOCDIR', docdir)
|
|
||||||
endif
|
|
||||||
|
|
||||||
conf.set('HAVE_JPEG_QS', libjpegqs.found())
|
conf.set('HAVE_JPEG_QS', libjpegqs.found())
|
||||||
conf.set('HAVE_LCMS2', lcms2.found())
|
conf.set('HAVE_LCMS2', lcms2.found())
|
||||||
@@ -146,6 +154,15 @@ if win32
|
|||||||
output : 'fiv.ico', input : icon_png_list,
|
output : 'fiv.ico', input : icon_png_list,
|
||||||
command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
|
command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
|
||||||
rc += windows.compile_resources('fiv.rc', depends : icon_ico)
|
rc += windows.compile_resources('fiv.rc', depends : icon_ico)
|
||||||
|
elif macos
|
||||||
|
# Meson is really extremely brain-dead and retarded.
|
||||||
|
# There is no real reason why this would have to be a shell script.
|
||||||
|
svg2icns = find_program('macos-svg2icns.sh')
|
||||||
|
icon_icns = custom_target('fiv.icns',
|
||||||
|
output : 'fiv.icns', input : 'fiv.svg',
|
||||||
|
command : [svg2icns, '@INPUT@', '@OUTPUT@'],
|
||||||
|
install : true,
|
||||||
|
install_dir : 'Contents/Resources')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
gnome = import('gnome')
|
gnome = import('gnome')
|
||||||
@@ -164,7 +181,8 @@ tiff_tables = custom_target('tiff-tables.h',
|
|||||||
)
|
)
|
||||||
|
|
||||||
desktops = ['fiv.desktop', 'fiv-browse.desktop']
|
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)
|
dependencies : dependencies).extract_all_objects(recursive : true)
|
||||||
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
|
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
|
||||||
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
|
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
|
||||||
@@ -212,13 +230,12 @@ foreach schema : gsettings_schemas
|
|||||||
input : schema,
|
input : schema,
|
||||||
output : application_ns + schema,
|
output : application_ns + schema,
|
||||||
copy : true,
|
copy : true,
|
||||||
install: true,
|
install : true,
|
||||||
install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
|
install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
|
||||||
endforeach
|
endforeach
|
||||||
|
|
||||||
# For the purposes of development: make the program find its GSettings schemas.
|
# For the purposes of development: make the program find its GSettings schemas.
|
||||||
gnome.compile_schemas(depend_files : files(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.
|
# Meson is broken on Windows and removes the backslashes, so this ends up empty.
|
||||||
symbolics = run_command(find_program('sed', required : false, disabler : true),
|
symbolics = run_command(find_program('sed', required : false, disabler : true),
|
||||||
@@ -254,7 +271,26 @@ install_data('fiv.svg',
|
|||||||
install_subdir('docs',
|
install_subdir('docs',
|
||||||
install_dir : docdir, strip_directory : true)
|
install_dir : docdir, strip_directory : true)
|
||||||
|
|
||||||
if not win32
|
if macos
|
||||||
|
# We're going all in on application bundles, seeing as it doesn't make
|
||||||
|
# much sense to install the application as in the block below.
|
||||||
|
#
|
||||||
|
# macOS has other mechanisms we can use to launch the JPEG cropper,
|
||||||
|
# or the reverse search utilities.
|
||||||
|
configure_file(
|
||||||
|
input : 'macos-Info.plist.in',
|
||||||
|
output : 'Info.plist',
|
||||||
|
configuration : rawconf,
|
||||||
|
install : true,
|
||||||
|
install_dir : 'Contents')
|
||||||
|
|
||||||
|
meson.add_install_script('macos-install.sh')
|
||||||
|
elif not win32
|
||||||
|
gnome.post_install(
|
||||||
|
glib_compile_schemas : true,
|
||||||
|
gtk_update_icon_cache : true,
|
||||||
|
)
|
||||||
|
|
||||||
asciidoctor = find_program('asciidoctor', required : false)
|
asciidoctor = find_program('asciidoctor', required : false)
|
||||||
a2x = find_program('a2x', required : false)
|
a2x = find_program('a2x', required : false)
|
||||||
if not asciidoctor.found() and not a2x.found()
|
if not asciidoctor.found() and not a2x.found()
|
||||||
@@ -355,16 +391,14 @@ elif meson.is_cross_build()
|
|||||||
wxs = configure_file(
|
wxs = configure_file(
|
||||||
input : 'fiv.wxs.in',
|
input : 'fiv.wxs.in',
|
||||||
output : 'fiv.wxs',
|
output : 'fiv.wxs',
|
||||||
configuration : configuration_data({
|
configuration : rawconf,
|
||||||
'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',
|
custom_target('package',
|
||||||
output : 'fiv.msi',
|
output : msi,
|
||||||
command : [meson.current_source_dir() / 'msys2-package.sh',
|
command : [meson.current_source_dir() / 'msys2-package.sh',
|
||||||
host_machine.cpu(), 'fiv.msi', wxs],
|
host_machine.cpu(), msi, wxs],
|
||||||
env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
|
env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
|
||||||
'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
|
'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
|
||||||
console : true,
|
console : true,
|
||||||
|
|||||||
@@ -75,10 +75,15 @@ extract() {
|
|||||||
--exclude '*/share/man' --exclude '*/share/doc'
|
--exclude '*/share/man' --exclude '*/share/doc'
|
||||||
done < db.want
|
done < db.want
|
||||||
|
|
||||||
bsdtar -xf exiftool.tar.gz
|
# Don't require Perl, which may not exist anymore on i686:
|
||||||
mv Image-ExifTool-*/exiftool bin
|
# https://github.com/msys2/MINGW-packages/pull/20085
|
||||||
mv Image-ExifTool-*/lib/* lib/perl5/site_perl
|
if [ -d lib/perl5 ]
|
||||||
rm -rf Image-ExifTool-*
|
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() {
|
configure() {
|
||||||
@@ -125,6 +130,8 @@ setup() {
|
|||||||
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
|
--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")}")
|
sourcedir=$(realpath "${2:-$(dirname "$0")}")
|
||||||
builddir=$(realpath "${1:-builddir}")
|
builddir=$(realpath "${1:-builddir}")
|
||||||
toolchain=$builddir/msys2-cross-toolchain.meson
|
toolchain=$builddir/msys2-cross-toolchain.meson
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ fi
|
|||||||
|
|
||||||
# Copy binaries we directly or indirectly depend on.
|
# Copy binaries we directly or indirectly depend on.
|
||||||
cp -p "$msys2_root"/bin/*.dll .
|
cp -p "$msys2_root"/bin/*.dll .
|
||||||
cp -p "$msys2_root"/bin/wperl.exe .
|
cp -p "$msys2_root"/bin/wperl.exe . || :
|
||||||
cp -p "$msys2_root"/bin/exiftool .
|
cp -p "$msys2_root"/bin/exiftool . || :
|
||||||
# The console helper is only useful for debug builds.
|
# The console helper is only useful for debug builds.
|
||||||
cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
|
cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
|
||||||
cp -pR "$msys2_root"/etc/ .
|
cp -pR "$msys2_root"/etc/ .
|
||||||
|
|
||||||
mkdir -p lib
|
mkdir -p lib
|
||||||
cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ 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
|
mkdir -p share/glib-2.0/schemas
|
||||||
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
|
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
|
||||||
mkdir -p share/icons
|
mkdir -p share/icons
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/sh -e
|
#!/bin/sh -e
|
||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
cd "$MESON_BUILD_ROOT"
|
cd "$MESON_BUILD_ROOT"
|
||||||
arch=$1 msi=$2 files=package-files.wxs destdir=$(pwd)/package
|
arch=$1 msi=$2 files=package-files.wxs
|
||||||
|
destdir=$(pwd)/package/${msi%.*}
|
||||||
shift 2
|
shift 2
|
||||||
|
|
||||||
# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
|
# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
|
||||||
@@ -15,7 +16,7 @@ txt2rtf() {
|
|||||||
print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
|
print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
|
||||||
print "\\f0\\fs24{\\pard\\sa240"
|
print "\\f0\\fs24{\\pard\\sa240"
|
||||||
} {
|
} {
|
||||||
gsub(/\\/, "\\\\"); gsub(/{/, "\\{"); gsub(/}/, "\\}")
|
gsub(/\\/, "\\\\"); gsub(/[{]/, "\\{"); gsub(/[}]/, "\\}")
|
||||||
if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
|
if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
|
||||||
else { print prefix $0; prefix = " " }
|
else { print prefix $0; prefix = " " }
|
||||||
} END {
|
} END {
|
||||||
|
|||||||
Submodule submodules/wuffs-mirror-release-c updated: c63c4a9348...50869df0ea
Reference in New Issue
Block a user