Compare commits
62 Commits
4b5b8ec9fa
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
|||
|
84269b2ba2
|
|||
|
51ca3f8e2e
|
|||
|
f196b03e97
|
|||
|
ee08565389
|
|||
|
c04c4063e4
|
|||
|
aed6ae6b83
|
|||
|
bae640a116
|
|||
|
52c17c8a16
|
|||
|
b07fba0c9c
|
|||
|
72bf913f3d
|
|||
|
e79574fd56
|
|||
|
93ad75eb35
|
|||
|
2d10aa8b61
|
|||
|
1ec41f7749
|
|||
|
d4b91d6260
|
|||
|
5ec5f5bdbd
|
|||
|
840e7f172c
|
|||
|
9b99de99bb
|
|||
|
ab75d2b61d
|
|||
|
92deba3890
|
|||
|
668c5eb78a
|
|||
|
d713d5820c
|
|||
|
f05e66bfc1
|
|||
|
6ee5f69bfe
|
|||
|
4249898497
|
|||
|
117422ade5
|
|||
|
8ff33e6b63
|
|||
|
ce4a13ed38
|
|||
|
6a1b851130
|
|||
|
68245b55c9
|
|||
|
2869c656c1
|
|||
|
ec713b633e
|
|||
|
88234f8283
|
|||
|
49ee551b9b
|
|||
|
089c90004b
|
|||
|
19913a5e48
|
|||
|
1ef0a84bc7
|
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||
Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
33
README.adoc
33
README.adoc
@@ -13,7 +13,7 @@ Features
|
||||
photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
|
||||
modules manage to load.
|
||||
- Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
|
||||
- Makes use of 30-bit X.org visuals, whenever it's possible and appropriate.
|
||||
- Can make use of 30-bit X.org visuals, under certain conditions.
|
||||
- Has a notion of pages, and tries to load all included content within files.
|
||||
- Can keep the zoom and position when browsing, to help with comparing
|
||||
zoomed-in images.
|
||||
@@ -33,15 +33,20 @@ Not necessarily in this order.
|
||||
|
||||
Packages
|
||||
--------
|
||||
Regular releases are sporadic. git master should be stable enough. You can get
|
||||
a package with the latest development version from Archlinux's AUR.
|
||||
Regular releases are sporadic. git master should be stable enough.
|
||||
You can get a package with the latest development version using Arch Linux's
|
||||
https://aur.archlinux.org/packages/fiv-git[AUR],
|
||||
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
|
||||
|
||||
https://janouch.name/cd[Windows installers can be found here],
|
||||
you want the _x86_64_ version.
|
||||
|
||||
Building and Running
|
||||
--------------------
|
||||
Build-only dependencies:
|
||||
Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
|
||||
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
|
||||
libturbojpeg, libwebp, librsvg-2.0 (for icons) +
|
||||
libturbojpeg, libwebp, libepoxy, librsvg-2.0 (for icons) +
|
||||
Optional dependencies: lcms2, Little CMS fast float plugin,
|
||||
LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
|
||||
resvg (unstable API, needs to be requested explicitly) +
|
||||
@@ -49,30 +54,34 @@ Runtime dependencies for reverse image search:
|
||||
xdg-utils, cURL, jq
|
||||
|
||||
$ git clone --recursive https://git.janouch.name/p/fiv.git
|
||||
$ cd fiv
|
||||
$ meson setup builddir
|
||||
$ cd builddir
|
||||
$ meson compile
|
||||
|
||||
Considering the vast amount of dynamically-linked dependencies, do not attempt
|
||||
direct installations via `ninja install`. To test the program:
|
||||
|
||||
$ meson devenv fiv
|
||||
|
||||
The lossless JPEG cropper and reverse image search are intended to be invoked
|
||||
from a context menu.
|
||||
from a file manager context menu.
|
||||
|
||||
For proper integration, you will need to install the application. On Debian,
|
||||
you can get a quick and dirty installation package for testing purposes using:
|
||||
|
||||
$ meson compile deb
|
||||
# dpkg -i fiv-*.deb
|
||||
|
||||
Windows
|
||||
~~~~~~~
|
||||
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
|
||||
dependencies listed at the beginning of 'msys2-configure.sh',
|
||||
plus rsvg-convert from librsvg2, and icotool from icoutils.
|
||||
plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
|
||||
Beware that the build will take up about a gigabyte of disk space.
|
||||
|
||||
$ sh -e msys2-configure.sh builddir
|
||||
$ meson install -C builddir
|
||||
$ meson compile package -C builddir
|
||||
|
||||
If everything succeeds, you will find a portable build of the application
|
||||
in the 'builddir/package' subdirectory. No installer is provided yet.
|
||||
in the 'builddir/package' subdirectory, and a very basic MSI installation
|
||||
package in 'builddir'.
|
||||
|
||||
Faster colour management
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -32,6 +32,10 @@ Options
|
||||
handler to implement the "Open Containing Folder" feature of certain
|
||||
applications.
|
||||
|
||||
*--collection*::
|
||||
Always put arguments in a virtual directory, even when only one is passed.
|
||||
Implies *--browse*.
|
||||
|
||||
*--help-all*::
|
||||
Show the full list of options, including those provided by GTK+.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ q:lang(en):after { content: "’"; }
|
||||
<p class="details">
|
||||
<span id="author">Přemysl Eric Janouch</span><br>
|
||||
<span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br>
|
||||
<span id="revnumber">version 0.0.0,</span>
|
||||
<span id="revnumber">version 1.0.0,</span>
|
||||
<span id="revdate">2023-04-17</span>
|
||||
|
||||
<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
|
||||
|
||||
BIN
docs/fiv.webp
BIN
docs/fiv.webp
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 194 KiB |
114
fiv-browser.c
114
fiv-browser.c
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-browser.c: filesystem browsing widget
|
||||
//
|
||||
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -17,9 +17,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <pixman.h>
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
@@ -27,6 +24,10 @@
|
||||
#ifdef GDK_WINDOWING_QUARTZ
|
||||
#include <gdk/gdkquartz.h>
|
||||
#endif // GDK_WINDOWING_QUARTZ
|
||||
#include <pixman.h>
|
||||
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "fiv-browser.h"
|
||||
#include "fiv-collection.h"
|
||||
@@ -91,7 +92,8 @@ struct _FivBrowser {
|
||||
|
||||
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
|
||||
size_t thumbnailers_len; ///< Thumbnailers array size
|
||||
GQueue thumbnailers_queue; ///< Queued up Entry pointers
|
||||
GQueue thumbnailers_queue_1; ///< Queued up Entry pointers, hi-prio
|
||||
GQueue thumbnailers_queue_2; ///< Queued up Entry pointers, lo-prio
|
||||
|
||||
GdkCursor *pointer; ///< Cached pointer cursor
|
||||
cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners
|
||||
@@ -240,10 +242,12 @@ relayout(FivBrowser *self, int width)
|
||||
pango_layout_set_wrap(label, PANGO_WRAP_WORD_CHAR);
|
||||
pango_layout_set_ellipsize(label, PANGO_ELLIPSIZE_END);
|
||||
|
||||
#if PANGO_VERSION_CHECK(1, 44, 0)
|
||||
PangoAttrList *attrs = pango_attr_list_new();
|
||||
pango_attr_list_insert(attrs, pango_attr_insert_hyphens_new(FALSE));
|
||||
pango_layout_set_attributes(label, attrs);
|
||||
pango_attr_list_unref (attrs);
|
||||
#endif
|
||||
}
|
||||
|
||||
g_array_append_val(items, ((Item) {
|
||||
@@ -272,14 +276,13 @@ relayout(FivBrowser *self, int width)
|
||||
gtk_adjustment_set_page_size(self->hadjustment, width);
|
||||
}
|
||||
if (self->vadjustment) {
|
||||
int height = gtk_widget_get_allocated_height(widget);
|
||||
gtk_adjustment_set_lower(self->vadjustment, 0);
|
||||
gtk_adjustment_set_upper(self->vadjustment, total_height);
|
||||
gtk_adjustment_set_upper(self->vadjustment, MAX(height, total_height));
|
||||
gtk_adjustment_set_step_increment(self->vadjustment,
|
||||
self->item_height + self->item_spacing + 2 * self->item_border_y);
|
||||
gtk_adjustment_set_page_increment(
|
||||
self->vadjustment, gtk_widget_get_allocated_height(widget) * 0.9);
|
||||
gtk_adjustment_set_page_size(
|
||||
self->vadjustment, gtk_widget_get_allocated_height(widget));
|
||||
gtk_adjustment_set_page_increment(self->vadjustment, height * 0.9);
|
||||
gtk_adjustment_set_page_size(self->vadjustment, height);
|
||||
}
|
||||
return total_height;
|
||||
}
|
||||
@@ -740,7 +743,7 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
|
||||
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
|
||||
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
|
||||
(void *) (intptr_t) 1, NULL);
|
||||
g_queue_push_tail(&self->thumbnailers_queue, entry);
|
||||
g_queue_push_tail(&self->thumbnailers_queue_2, entry);
|
||||
}
|
||||
|
||||
entry_set_surface_user_data(entry);
|
||||
@@ -794,13 +797,21 @@ on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
|
||||
thumbnailer_next(t);
|
||||
}
|
||||
|
||||
// TODO(p): Try to keep the minions alive (stdout will be a problem).
|
||||
static gboolean
|
||||
thumbnailer_next(Thumbnailer *t)
|
||||
{
|
||||
// TODO(p): Try to keep the minions alive (stdout will be a problem).
|
||||
// Already have something to do, not a failure.
|
||||
if (t->target)
|
||||
return TRUE;
|
||||
|
||||
// They could have been removed via post-reload changes in the model.
|
||||
FivBrowser *self = t->self;
|
||||
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue)))
|
||||
return FALSE;
|
||||
do {
|
||||
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue_1)) &&
|
||||
!(t->target = g_queue_pop_head(&self->thumbnailers_queue_2)))
|
||||
return FALSE;
|
||||
} while (t->target->removed);
|
||||
|
||||
// Case analysis:
|
||||
// - We haven't found any thumbnail for the entry at all
|
||||
@@ -817,9 +828,18 @@ thumbnailer_next(Thumbnailer *t)
|
||||
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
|
||||
"--", uri, NULL};
|
||||
|
||||
GSubprocessLauncher *launcher =
|
||||
g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE);
|
||||
#ifdef G_OS_WIN32
|
||||
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
|
||||
g_subprocess_launcher_set_cwd(launcher, prefix);
|
||||
g_free(prefix);
|
||||
#endif
|
||||
|
||||
GError *error = NULL;
|
||||
t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower,
|
||||
G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
|
||||
t->minion = g_subprocess_launcher_spawnv(
|
||||
launcher, t->target->icon ? argv_faster : argv_slower, &error);
|
||||
g_object_unref(launcher);
|
||||
if (error) {
|
||||
g_warning("%s", error->message);
|
||||
g_error_free(error);
|
||||
@@ -837,7 +857,8 @@ thumbnailer_next(Thumbnailer *t)
|
||||
static void
|
||||
thumbnailers_abort(FivBrowser *self)
|
||||
{
|
||||
g_queue_clear(&self->thumbnailers_queue);
|
||||
g_queue_clear(&self->thumbnailers_queue_1);
|
||||
g_queue_clear(&self->thumbnailers_queue_2);
|
||||
|
||||
for (size_t i = 0; i < self->thumbnailers_len; i++) {
|
||||
Thumbnailer *t = self->thumbnailers + i;
|
||||
@@ -853,35 +874,35 @@ thumbnailers_abort(FivBrowser *self)
|
||||
}
|
||||
|
||||
static void
|
||||
thumbnailers_start(FivBrowser *self)
|
||||
thumbnailers_enqueue(FivBrowser *self, Entry *entry)
|
||||
{
|
||||
thumbnailers_abort(self);
|
||||
if (!self->model)
|
||||
return;
|
||||
|
||||
GQueue lq = G_QUEUE_INIT;
|
||||
for (guint i = 0; i < self->entries->len; i++) {
|
||||
Entry *entry = self->entries->pdata[i];
|
||||
if (entry->removed)
|
||||
continue;
|
||||
|
||||
if (!entry->removed) {
|
||||
if (entry->icon)
|
||||
g_queue_push_tail(&self->thumbnailers_queue, entry);
|
||||
g_queue_push_tail(&self->thumbnailers_queue_1, entry);
|
||||
else if (cairo_surface_get_user_data(
|
||||
entry->thumbnail, &fiv_thumbnail_key_lq))
|
||||
g_queue_push_tail(&lq, entry);
|
||||
}
|
||||
while (!g_queue_is_empty(&lq)) {
|
||||
g_queue_push_tail_link(
|
||||
&self->thumbnailers_queue, g_queue_pop_head_link(&lq));
|
||||
g_queue_push_tail(&self->thumbnailers_queue_2, entry);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
thumbnailers_deploy(FivBrowser *self)
|
||||
{
|
||||
for (size_t i = 0; i < self->thumbnailers_len; i++) {
|
||||
if (!thumbnailer_next(self->thumbnailers + i))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
thumbnailers_restart(FivBrowser *self)
|
||||
{
|
||||
thumbnailers_abort(self);
|
||||
for (guint i = 0; i < self->entries->len; i++)
|
||||
thumbnailers_enqueue(self, self->entries->pdata[i]);
|
||||
thumbnailers_deploy(self);
|
||||
}
|
||||
|
||||
// --- Boilerplate -------------------------------------------------------------
|
||||
|
||||
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
|
||||
@@ -1002,7 +1023,7 @@ set_item_size(FivBrowser *self, FivThumbnailSize size)
|
||||
|
||||
g_hash_table_remove_all(self->thumbnail_cache);
|
||||
reload_thumbnails(self);
|
||||
thumbnailers_start(self);
|
||||
thumbnailers_restart(self);
|
||||
|
||||
g_object_notify_by_pspec(
|
||||
G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
|
||||
@@ -1560,6 +1581,14 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
|
||||
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
|
||||
case 0:
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_Delete:
|
||||
if (self->selected) {
|
||||
GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
|
||||
GFile *file = g_file_new_for_uri(self->selected->e->uri);
|
||||
fiv_context_menu_remove(window, file);
|
||||
g_object_unref(file);
|
||||
}
|
||||
return GDK_EVENT_STOP;
|
||||
case GDK_KEY_Return:
|
||||
if (self->selected)
|
||||
return open_entry(widget, self->selected, FALSE);
|
||||
@@ -1862,7 +1891,8 @@ fiv_browser_init(FivBrowser *self)
|
||||
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
|
||||
for (size_t i = 0; i < self->thumbnailers_len; i++)
|
||||
self->thumbnailers[i].self = self;
|
||||
g_queue_init(&self->thumbnailers_queue);
|
||||
g_queue_init(&self->thumbnailers_queue_1);
|
||||
g_queue_init(&self->thumbnailers_queue_2);
|
||||
|
||||
set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL);
|
||||
self->show_labels = FALSE;
|
||||
@@ -1907,8 +1937,9 @@ on_model_reloaded(FivIoModel *model, FivBrowser *self)
|
||||
fiv_browser_select(self, selected_uri);
|
||||
g_free(selected_uri);
|
||||
|
||||
// Restarting thumbnailers is critical, because they keep Entry pointers.
|
||||
reload_thumbnails(self);
|
||||
thumbnailers_start(self);
|
||||
thumbnailers_restart(self);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -1923,8 +1954,8 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
|
||||
g_ptr_array_add(self->entries, entry);
|
||||
|
||||
reload_one_thumbnail(self, entry);
|
||||
// TODO(p): Try to add to thumbnailer queue if already started.
|
||||
thumbnailers_start(self);
|
||||
thumbnailers_enqueue(self, entry);
|
||||
thumbnailers_deploy(self);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1950,8 +1981,9 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
|
||||
// so that there's no jumping around. Or, a bit more properly,
|
||||
// move the thumbnail cache entry to the new URI.
|
||||
reload_one_thumbnail(self, found);
|
||||
// TODO(p): Try to add to thumbnailer queue if already started.
|
||||
thumbnailers_start(self);
|
||||
// TODO(p): Rather cancel the entry in any running thumbnailer,
|
||||
// remove it from queues, and _enqueue() + _deploy().
|
||||
thumbnailers_restart(self);
|
||||
} else {
|
||||
found->removed = TRUE;
|
||||
gtk_widget_queue_draw(GTK_WIDGET(self));
|
||||
|
||||
@@ -528,12 +528,16 @@ fiv_collection_file_query_info(GFile *file, const char *attributes,
|
||||
g_file_info_set_name(info, basename);
|
||||
g_free(basename);
|
||||
|
||||
if ((name = g_file_info_get_display_name(info))) {
|
||||
if (g_file_info_has_attribute(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) &&
|
||||
(name = g_file_info_get_display_name(info))) {
|
||||
gchar *prefixed = get_prefixed_name(self, name);
|
||||
g_file_info_set_display_name(info, prefixed);
|
||||
g_free(prefixed);
|
||||
}
|
||||
if ((name = g_file_info_get_edit_name(info))) {
|
||||
if (g_file_info_has_attribute(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME) &&
|
||||
(name = g_file_info_get_edit_name(info))) {
|
||||
gchar *prefixed = get_prefixed_name(self, name);
|
||||
g_file_info_set_edit_name(info, prefixed);
|
||||
g_free(prefixed);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-context-menu.c: popup menu
|
||||
//
|
||||
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -185,15 +185,24 @@ info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
|
||||
if (bytes_in)
|
||||
flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
|
||||
|
||||
GSubprocessLauncher *launcher = g_subprocess_launcher_new(flags);
|
||||
#ifdef G_OS_WIN32
|
||||
// Both to find wperl, and then to let wperl find the nearby exiftool.
|
||||
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
|
||||
g_subprocess_launcher_set_cwd(launcher, prefix);
|
||||
g_free(prefix);
|
||||
#endif
|
||||
|
||||
// TODO(p): Add a fallback to internal capabilities.
|
||||
// The simplest is to specify the filename and the resolution.
|
||||
GError *error = NULL;
|
||||
GSubprocess *subprocess = g_subprocess_new(flags, &error,
|
||||
GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
|
||||
#ifdef G_OS_WIN32
|
||||
"wperl",
|
||||
#endif
|
||||
"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
|
||||
"--binary", "-quiet", "--", path, NULL);
|
||||
g_object_unref(launcher);
|
||||
if (error) {
|
||||
info_redirect_error(dialog, error);
|
||||
return;
|
||||
@@ -328,17 +337,13 @@ open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
|
||||
}
|
||||
|
||||
static void
|
||||
open_context_show_error_dialog(OpenContext *self, GError *error)
|
||||
show_error_dialog(GtkWindow *parent, GError *error)
|
||||
{
|
||||
GtkWindow *window = g_weak_ref_get(&self->window);
|
||||
|
||||
GtkWidget *dialog =
|
||||
gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL,
|
||||
gtk_message_dialog_new(GTK_WINDOW(parent), GTK_DIALOG_MODAL,
|
||||
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
|
||||
gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
gtk_widget_destroy(dialog);
|
||||
|
||||
g_clear_object(&window);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
@@ -357,7 +362,9 @@ open_context_launch(GtkWidget *widget, OpenContext *self)
|
||||
(void) g_app_info_set_as_last_used_for_type(
|
||||
self->app_info, self->content_type, NULL);
|
||||
} else {
|
||||
open_context_show_error_dialog(self, error);
|
||||
GtkWindow *window = g_weak_ref_get(&self->window);
|
||||
show_error_dialog(window, error);
|
||||
g_clear_object(&window);
|
||||
}
|
||||
g_list_free(files);
|
||||
g_object_unref(context);
|
||||
@@ -437,14 +444,22 @@ on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
|
||||
g_free(uri);
|
||||
}
|
||||
|
||||
void
|
||||
fiv_context_menu_remove(GtkWindow *parent, GFile *file)
|
||||
{
|
||||
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
|
||||
GError *error = NULL;
|
||||
if (!g_file_trash(file, NULL, &error))
|
||||
show_error_dialog(parent, error);
|
||||
}
|
||||
|
||||
static void
|
||||
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
|
||||
{
|
||||
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
|
||||
OpenContext *ctx = user_data;
|
||||
GError *error = NULL;
|
||||
if (!g_file_trash(ctx->file, NULL, &error))
|
||||
open_context_show_error_dialog(ctx, error);
|
||||
GtkWindow *window = g_weak_ref_get(&ctx->window);
|
||||
fiv_context_menu_remove(window, ctx->file);
|
||||
g_clear_object(&window);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
@@ -541,7 +556,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
|
||||
gtk_menu_shell_append(
|
||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||
|
||||
item = gtk_menu_item_new_with_mnemonic("_Information...");
|
||||
item = gtk_menu_item_new_with_mnemonic("_Information");
|
||||
g_signal_connect_data(item, "activate", G_CALLBACK(on_info_activate),
|
||||
g_rc_box_acquire(ctx), open_context_unref, 0);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-context-menu.h: popup menu
|
||||
//
|
||||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -18,4 +18,5 @@
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
|
||||
void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
|
||||
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);
|
||||
|
||||
463
fiv-io-cmm.c
Normal file
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(
|
||||
FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
|
||||
{
|
||||
if (self->filtering && g_file_info_get_is_hidden(info))
|
||||
if (self->filtering &&
|
||||
g_file_info_has_attribute(info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
|
||||
g_file_info_get_is_hidden(info))
|
||||
return NULL;
|
||||
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
|
||||
return subdirs;
|
||||
@@ -346,6 +348,8 @@ static void
|
||||
monitor_apply(enum monitor_event event, GPtrArray *target, int index,
|
||||
FivIoModelEntry *new_entry)
|
||||
{
|
||||
g_return_if_fail(event != MONITOR_CHANGING || index >= 0);
|
||||
|
||||
if (event == MONITOR_RENAMING && index < 0)
|
||||
// The file used to be filtered out but isn't anymore.
|
||||
event = MONITOR_ADDING;
|
||||
@@ -706,7 +710,7 @@ fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
|
||||
|
||||
GError *e = NULL;
|
||||
if ((self->monitor = g_file_monitor_directory(
|
||||
directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
|
||||
directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
|
||||
g_signal_connect(self->monitor, "changed",
|
||||
G_CALLBACK(on_monitor_changed), self);
|
||||
} else {
|
||||
|
||||
563
fiv-io.c
563
fiv-io.c
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-io.c: image operations
|
||||
//
|
||||
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -34,11 +34,6 @@
|
||||
#include <libjpegqs.h>
|
||||
#endif // HAVE_JPEG_QS
|
||||
|
||||
// Colour management must be handled before RGB conversions.
|
||||
#ifdef HAVE_LCMS2
|
||||
#include <lcms2.h>
|
||||
#endif // HAVE_LCMS2
|
||||
|
||||
#define TIFF_TABLES_CONSTANTS_ONLY
|
||||
#include "tiff-tables.h"
|
||||
#include "tiffer.h"
|
||||
@@ -47,6 +42,8 @@
|
||||
#include <libraw.h>
|
||||
#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
|
||||
#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0
|
||||
#else
|
||||
#define rawparams params
|
||||
#endif
|
||||
#endif // HAVE_LIBRAW
|
||||
#ifdef HAVE_RESVG
|
||||
@@ -64,6 +61,9 @@
|
||||
#ifdef HAVE_LIBTIFF
|
||||
#include <tiff.h>
|
||||
#include <tiffio.h>
|
||||
#ifndef TIFF_TMSIZE_T_MAX
|
||||
#define TIFF_TMSIZE_T_MAX ((tmsize_t) (SIZE_MAX >> 1))
|
||||
#endif
|
||||
#endif // HAVE_LIBTIFF
|
||||
#ifdef HAVE_GDKPIXBUF
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
@@ -191,12 +191,14 @@ fiv_io_image_new(cairo_format_t format, uint32_t width, uint32_t height)
|
||||
case CAIRO_FORMAT_ARGB32:
|
||||
unit = 4;
|
||||
break;
|
||||
#if CAIRO_VERSION >= 11702
|
||||
case CAIRO_FORMAT_RGB96F:
|
||||
unit = 12;
|
||||
break;
|
||||
case CAIRO_FORMAT_RGBA128F:
|
||||
unit = 16;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
@@ -289,318 +291,6 @@ try_append_page(
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Colour management -------------------------------------------------------
|
||||
|
||||
FivIoProfile
|
||||
fiv_io_profile_new(const void *data, size_t len)
|
||||
{
|
||||
#ifdef HAVE_LCMS2
|
||||
return cmsOpenProfileFromMem(data, len);
|
||||
#else
|
||||
(void) data;
|
||||
(void) len;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
FivIoProfile
|
||||
fiv_io_profile_new_sRGB(void)
|
||||
{
|
||||
#ifdef HAVE_LCMS2
|
||||
return cmsCreate_sRGBProfile();
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
FivIoProfile
|
||||
fiv_io_profile_new_sRGB_gamma(double gamma)
|
||||
{
|
||||
#ifdef HAVE_LCMS2
|
||||
// TODO(p): Make sure to use the library in a thread-safe manner.
|
||||
cmsContext context = NULL;
|
||||
|
||||
static const cmsCIExyY D65 = {0.3127, 0.3290, 1.0};
|
||||
static const cmsCIExyYTRIPLE primaries = {
|
||||
{0.6400, 0.3300, 1.0}, {0.3000, 0.6000, 1.0}, {0.1500, 0.0600, 1.0}};
|
||||
cmsToneCurve *curve = cmsBuildGamma(context, gamma);
|
||||
if (!curve)
|
||||
return NULL;
|
||||
|
||||
cmsHPROFILE profile = cmsCreateRGBProfileTHR(
|
||||
context, &D65, &primaries, (cmsToneCurve *[3]){curve, curve, curve});
|
||||
cmsFreeToneCurve(curve);
|
||||
return profile;
|
||||
#else
|
||||
(void) gamma;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static FivIoProfile
|
||||
fiv_io_profile_new_from_bytes(GBytes *bytes)
|
||||
{
|
||||
gsize len = 0;
|
||||
gconstpointer p = g_bytes_get_data(bytes, &len);
|
||||
return fiv_io_profile_new(p, len);
|
||||
}
|
||||
|
||||
static GBytes *
|
||||
fiv_io_profile_to_bytes(FivIoProfile profile)
|
||||
{
|
||||
#ifdef HAVE_LCMS2
|
||||
cmsUInt32Number len = 0;
|
||||
(void) cmsSaveProfileToMem(profile, NULL, &len);
|
||||
gchar *data = g_malloc0(len);
|
||||
if (!cmsSaveProfileToMem(profile, data, &len)) {
|
||||
g_free(data);
|
||||
return NULL;
|
||||
}
|
||||
return g_bytes_new_take(data, len);
|
||||
#else
|
||||
(void) profile;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
fiv_io_profile_free(FivIoProfile self)
|
||||
{
|
||||
#ifdef HAVE_LCMS2
|
||||
cmsCloseProfile(self);
|
||||
#else
|
||||
(void) self;
|
||||
#endif
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
|
||||
#ifndef HAVE_LCMS2
|
||||
#define FIV_IO_PROFILE_ARGB32 0
|
||||
#define FIV_IO_PROFILE_4X16LE 0
|
||||
#else
|
||||
#define FIV_IO_PROFILE_ARGB32 \
|
||||
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
|
||||
#define FIV_IO_PROFILE_4X16LE \
|
||||
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
|
||||
#endif
|
||||
|
||||
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
|
||||
// ARGB/BGRA/XRGB/BGRX.
|
||||
static void
|
||||
trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
|
||||
{
|
||||
// This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
|
||||
// It will typically produce horribly oversaturated results.
|
||||
// Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop
|
||||
// does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096
|
||||
while (len--) {
|
||||
int c = p[0], m = p[1], y = p[2], k = p[3];
|
||||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||||
p[0] = k * y / 255;
|
||||
p[1] = k * m / 255;
|
||||
p[2] = k * c / 255;
|
||||
p[3] = 255;
|
||||
#else
|
||||
p[3] = k * y / 255;
|
||||
p[2] = k * m / 255;
|
||||
p[1] = k * c / 255;
|
||||
p[0] = 255;
|
||||
#endif
|
||||
p += 4;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_profile_cmyk(
|
||||
FivIoImage *image, FivIoProfile source, FivIoProfile target)
|
||||
{
|
||||
#ifndef HAVE_LCMS2
|
||||
(void) source;
|
||||
(void) target;
|
||||
#else
|
||||
cmsHTRANSFORM transform = NULL;
|
||||
if (source && target) {
|
||||
transform = cmsCreateTransform(source, TYPE_CMYK_8_REV, target,
|
||||
FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
|
||||
}
|
||||
if (transform) {
|
||||
cmsDoTransform(
|
||||
transform, image->data, image->data, image->width * image->height);
|
||||
cmsDeleteTransform(transform);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
trivial_cmyk_to_host_byte_order_argb(
|
||||
image->data, image->width * image->height);
|
||||
}
|
||||
|
||||
static bool
|
||||
fiv_io_profile_rgb_direct(unsigned char *data, int w, int h,
|
||||
FivIoProfile source, FivIoProfile target,
|
||||
uint32_t source_format, uint32_t target_format)
|
||||
{
|
||||
#ifndef HAVE_LCMS2
|
||||
(void) data;
|
||||
(void) w;
|
||||
(void) h;
|
||||
(void) source;
|
||||
(void) source_format;
|
||||
(void) target;
|
||||
(void) target_format;
|
||||
return false;
|
||||
#else
|
||||
// TODO(p): We should make this optional.
|
||||
cmsHPROFILE src_fallback = NULL;
|
||||
if (target && !source)
|
||||
source = src_fallback = cmsCreate_sRGBProfile();
|
||||
|
||||
cmsHTRANSFORM transform = NULL;
|
||||
if (source && target) {
|
||||
transform = cmsCreateTransform(
|
||||
source, source_format, target, target_format, INTENT_PERCEPTUAL, 0);
|
||||
}
|
||||
if (transform) {
|
||||
cmsDoTransform(transform, data, data, w * h);
|
||||
cmsDeleteTransform(transform);
|
||||
}
|
||||
if (src_fallback)
|
||||
cmsCloseProfile(src_fallback);
|
||||
return transform != NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_profile_xrgb32(
|
||||
FivIoImage *image, FivIoProfile source, FivIoProfile target)
|
||||
{
|
||||
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
|
||||
source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_profile_4x16le_direct(
|
||||
unsigned char *data, int w, int h, FivIoProfile source, FivIoProfile target)
|
||||
{
|
||||
fiv_io_profile_rgb_direct(data, w, h, source, target,
|
||||
FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
fiv_io_profile_page(FivIoImage *page, FivIoProfile target,
|
||||
void (*frame_cb) (FivIoImage *, FivIoProfile, FivIoProfile))
|
||||
{
|
||||
FivIoProfile source = NULL;
|
||||
if (page->icc)
|
||||
source = fiv_io_profile_new_from_bytes(page->icc);
|
||||
|
||||
// TODO(p): All animations need to be composited in a linear colour space.
|
||||
for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
|
||||
frame_cb(frame, source, target);
|
||||
|
||||
if (source)
|
||||
fiv_io_profile_free(source);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_premultiply_argb32(FivIoImage *image)
|
||||
{
|
||||
if (image->format != CAIRO_FORMAT_ARGB32)
|
||||
return;
|
||||
|
||||
for (uint32_t y = 0; y < image->height; y++) {
|
||||
uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
|
||||
for (uint32_t x = 0; x < image->width; x++) {
|
||||
uint32_t argb = dstp[x], a = argb >> 24;
|
||||
dstp[x] = a << 24 |
|
||||
PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
|
||||
PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 |
|
||||
PREMULTIPLY8(a, 0xFF & argb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
|
||||
|
||||
#define FIV_IO_PROFILE_ARGB32_PREMUL \
|
||||
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
|
||||
|
||||
static void
|
||||
fiv_io_profile_argb32(FivIoImage *image,
|
||||
FivIoProfile source, FivIoProfile target)
|
||||
{
|
||||
g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
|
||||
|
||||
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
|
||||
source, target,
|
||||
FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_profile_argb32_premultiply(
|
||||
FivIoImage *image, FivIoProfile source, FivIoProfile target)
|
||||
{
|
||||
if (image->format != CAIRO_FORMAT_ARGB32) {
|
||||
fiv_io_profile_xrgb32(image, source, target);
|
||||
} else if (!fiv_io_profile_rgb_direct(image->data,
|
||||
image->width, image->height, source, target,
|
||||
FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
|
||||
g_debug("failed to create a premultiplying transform");
|
||||
fiv_io_premultiply_argb32(image);
|
||||
}
|
||||
}
|
||||
|
||||
#else // ! HAVE_LCMS2 || LCMS_VERSION < 2130
|
||||
|
||||
// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
|
||||
#define fiv_io_profile_argb32(surface, source, target)
|
||||
|
||||
static void
|
||||
fiv_io_profile_argb32_premultiply(
|
||||
FivIoImage *image, FivIoProfile source, FivIoProfile target)
|
||||
{
|
||||
fiv_io_profile_xrgb32(image, source, target);
|
||||
fiv_io_premultiply_argb32(image);
|
||||
}
|
||||
|
||||
#endif // ! HAVE_LCMS2 || LCMS_VERSION < 2130
|
||||
|
||||
#define fiv_io_profile_argb32_premultiply_page(page, target) \
|
||||
fiv_io_profile_page((page), (target), fiv_io_profile_argb32_premultiply)
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
fiv_io_profile_any(FivIoImage *image, FivIoProfile source, FivIoProfile target)
|
||||
{
|
||||
// TODO(p): Ensure we do colour management early enough, so that
|
||||
// no avoidable increase of quantization error occurs beforehands,
|
||||
// and also for correct alpha compositing.
|
||||
switch (image->format) {
|
||||
break; case CAIRO_FORMAT_RGB24:
|
||||
fiv_io_profile_xrgb32(image, source, target);
|
||||
break; case CAIRO_FORMAT_ARGB32:
|
||||
fiv_io_profile_argb32(image, source, target);
|
||||
break; default:
|
||||
g_debug("CM attempted on an unsupported surface format");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
|
||||
static FivIoImage *
|
||||
fiv_io_profile_finalize(FivIoImage *image, FivIoProfile target)
|
||||
{
|
||||
if (!target)
|
||||
return image;
|
||||
|
||||
for (FivIoImage *page = image; page != NULL; page = page->page_next)
|
||||
fiv_io_profile_page(page, target, fiv_io_profile_any);
|
||||
return image;
|
||||
}
|
||||
|
||||
// --- Wuffs -------------------------------------------------------------------
|
||||
|
||||
static bool
|
||||
@@ -694,8 +384,9 @@ struct load_wuffs_frame_context {
|
||||
GBytes *meta_iccp; ///< Reference-counted ICC profile
|
||||
GBytes *meta_xmp; ///< Reference-counted XMP
|
||||
|
||||
FivIoProfile target; ///< Target device profile, if any
|
||||
FivIoProfile source; ///< Source colour profile, if any
|
||||
FivIoCmm *cmm; ///< CMM context, if any
|
||||
FivIoProfile *target; ///< Target device profile, if any
|
||||
FivIoProfile *source; ///< Source colour profile, if any
|
||||
|
||||
FivIoImage *result; ///< The resulting image (referenced)
|
||||
FivIoImage *result_tail; ///< The final animation frame
|
||||
@@ -762,11 +453,12 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
|
||||
|
||||
if (ctx->target) {
|
||||
if (ctx->expand_16_float || ctx->pack_16_10) {
|
||||
fiv_io_profile_4x16le_direct(
|
||||
fiv_io_cmm_4x16le_direct(ctx->cmm,
|
||||
targetbuf, ctx->width, ctx->height, ctx->source, ctx->target);
|
||||
// The first one premultiplies below, the second doesn't need to.
|
||||
} else {
|
||||
fiv_io_profile_argb32_premultiply(image, ctx->source, ctx->target);
|
||||
fiv_io_cmm_argb32_premultiply(
|
||||
ctx->cmm, image, ctx->source, ctx->target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,7 +596,8 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
|
||||
const FivIoOpenContext *ioctx, GError **error)
|
||||
{
|
||||
struct load_wuffs_frame_context ctx = {
|
||||
.dec = dec, .src = &src, .target = ioctx->screen_profile};
|
||||
.dec = dec, .src = &src,
|
||||
.cmm = ioctx->cmm, .target = ioctx->screen_profile};
|
||||
|
||||
// TODO(p): PNG text chunks, like we do with PNG thumbnails.
|
||||
// TODO(p): See if something could and should be done about
|
||||
@@ -988,9 +681,11 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
|
||||
// TODO(p): Improve our simplistic PNG handling of: gAMA, cHRM, sRGB.
|
||||
if (ctx.target) {
|
||||
if (ctx.meta_iccp)
|
||||
ctx.source = fiv_io_profile_new_from_bytes(ctx.meta_iccp);
|
||||
ctx.source = fiv_io_cmm_get_profile_from_bytes(
|
||||
ctx.cmm, ctx.meta_iccp);
|
||||
else if (isfinite(gamma) && gamma > 0)
|
||||
ctx.source = fiv_io_profile_new_sRGB_gamma(gamma);
|
||||
ctx.source = fiv_io_cmm_get_profile_sRGB_gamma(
|
||||
ctx.cmm, gamma);
|
||||
}
|
||||
|
||||
// Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine.
|
||||
@@ -1281,7 +976,7 @@ static uint32_t *
|
||||
parse_mpf_index_entries(const struct tiffer *T, struct tiffer_entry *entry)
|
||||
{
|
||||
uint32_t count = entry->remaining_count / 16;
|
||||
uint32_t *offsets = g_malloc0_n(sizeof *offsets, count + 1), *out = offsets;
|
||||
uint32_t *offsets = g_malloc0_n(count + 1, sizeof *offsets), *out = offsets;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
// 5.2.3.3.3. Individual Image Data Offset
|
||||
uint32_t offset = parse_mpf_mpentry(entry->p + i * 16, T);
|
||||
@@ -1327,6 +1022,100 @@ parse_mpf(
|
||||
|
||||
// --- JPEG --------------------------------------------------------------------
|
||||
|
||||
struct exif_profile {
|
||||
double whitepoint[2]; ///< TIFF_WhitePoint
|
||||
double primaries[6]; ///< TIFF_PrimaryChromaticities
|
||||
enum Exif_ColorSpace colorspace; ///< Exif_ColorSpace
|
||||
double gamma; ///< Exif_Gamma
|
||||
|
||||
bool have_whitepoint;
|
||||
bool have_primaries;
|
||||
bool have_colorspace;
|
||||
bool have_gamma;
|
||||
};
|
||||
|
||||
static bool
|
||||
parse_exif_profile_reals(
|
||||
const struct tiffer *T, struct tiffer_entry *entry, double *out)
|
||||
{
|
||||
while (tiffer_real(T, entry, out++))
|
||||
if (!tiffer_next_value(entry))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
parse_exif_profile_subifd(
|
||||
struct exif_profile *params, const struct tiffer *T, uint32_t offset)
|
||||
{
|
||||
struct tiffer subT = {};
|
||||
if (!tiffer_subifd(T, offset, &subT))
|
||||
return;
|
||||
|
||||
struct tiffer_entry entry = {};
|
||||
while (tiffer_next_entry(&subT, &entry)) {
|
||||
int64_t value = 0;
|
||||
if (G_UNLIKELY(entry.tag == Exif_ColorSpace) &&
|
||||
entry.type == TIFFER_SHORT && entry.remaining_count == 1 &&
|
||||
tiffer_integer(&subT, &entry, &value)) {
|
||||
params->have_colorspace = true;
|
||||
params->colorspace = value;
|
||||
} else if (G_UNLIKELY(entry.tag == Exif_Gamma) &&
|
||||
entry.type == TIFFER_RATIONAL && entry.remaining_count == 1 &&
|
||||
tiffer_real(&subT, &entry, ¶ms->gamma)) {
|
||||
params->have_gamma = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static FivIoProfile *
|
||||
parse_exif_profile(FivIoCmm *cmm, const void *data, size_t len)
|
||||
{
|
||||
struct tiffer T = {};
|
||||
if (!tiffer_init(&T, (const uint8_t *) data, len) || !tiffer_next_ifd(&T))
|
||||
return NULL;
|
||||
|
||||
struct exif_profile params = {};
|
||||
struct tiffer_entry entry = {};
|
||||
while (tiffer_next_entry(&T, &entry)) {
|
||||
int64_t offset = 0;
|
||||
if (G_UNLIKELY(entry.tag == TIFF_ExifIFDPointer) &&
|
||||
entry.type == TIFFER_LONG && entry.remaining_count == 1 &&
|
||||
tiffer_integer(&T, &entry, &offset) &&
|
||||
offset >= 0 && offset <= UINT32_MAX) {
|
||||
parse_exif_profile_subifd(¶ms, &T, offset);
|
||||
} else if (G_UNLIKELY(entry.tag == TIFF_WhitePoint) &&
|
||||
entry.type == TIFFER_RATIONAL &&
|
||||
entry.remaining_count == G_N_ELEMENTS(params.whitepoint)) {
|
||||
params.have_whitepoint =
|
||||
parse_exif_profile_reals(&T, &entry, params.whitepoint);
|
||||
} else if (G_UNLIKELY(entry.tag == TIFF_PrimaryChromaticities) &&
|
||||
entry.type == TIFFER_RATIONAL &&
|
||||
entry.remaining_count == G_N_ELEMENTS(params.primaries)) {
|
||||
params.have_primaries =
|
||||
parse_exif_profile_reals(&T, &entry, params.primaries);
|
||||
}
|
||||
}
|
||||
if (!params.have_colorspace)
|
||||
return NULL;
|
||||
|
||||
// If sRGB is claimed, assume all parameters are standard.
|
||||
if (params.colorspace == Exif_ColorSpace_sRGB)
|
||||
return fiv_io_cmm_get_profile_sRGB(cmm);
|
||||
|
||||
// AdobeRGB Nikon JPEGs provide all of these.
|
||||
if (params.colorspace != Exif_ColorSpace_Uncalibrated ||
|
||||
!params.have_gamma ||
|
||||
!params.have_whitepoint ||
|
||||
!params.have_primaries)
|
||||
return NULL;
|
||||
|
||||
return fiv_io_cmm_get_profile_parametric(cmm,
|
||||
params.gamma, params.whitepoint, params.primaries);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct jpeg_metadata {
|
||||
GByteArray *exif; ///< Exif buffer or NULL
|
||||
GByteArray *icc; ///< ICC profile buffer or NULL
|
||||
@@ -1475,15 +1264,18 @@ load_jpeg_finalize(FivIoImage *image, bool cmyk,
|
||||
else
|
||||
g_byte_array_free(meta.icc, TRUE);
|
||||
|
||||
FivIoProfile source = NULL;
|
||||
if (icc_profile)
|
||||
source = fiv_io_profile_new(
|
||||
FivIoProfile *source = NULL;
|
||||
if (icc_profile && ctx->cmm)
|
||||
source = fiv_io_cmm_get_profile(ctx->cmm,
|
||||
g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile));
|
||||
else if (image->exif && ctx->cmm)
|
||||
source = parse_exif_profile(ctx->cmm,
|
||||
g_bytes_get_data(image->exif, NULL), g_bytes_get_size(image->exif));
|
||||
|
||||
if (cmyk)
|
||||
fiv_io_profile_cmyk(image, source, ctx->screen_profile);
|
||||
fiv_io_cmm_cmyk(ctx->cmm, image, source, ctx->screen_profile);
|
||||
else
|
||||
fiv_io_profile_any(image, source, ctx->screen_profile);
|
||||
fiv_io_cmm_any(ctx->cmm, image, source, ctx->screen_profile);
|
||||
|
||||
if (source)
|
||||
fiv_io_profile_free(source);
|
||||
@@ -1536,6 +1328,8 @@ load_libjpeg_turbo(const char *data, gsize len, const FivIoOpenContext *ctx,
|
||||
jpeg_create_decompress(&cinfo);
|
||||
jpeg_mem_src(&cinfo, (const unsigned char *) data, len);
|
||||
(void) jpeg_read_header(&cinfo, true);
|
||||
// TODO(p): With newer libjpeg-turbo, if cinfo.data_precision is 12 or 16,
|
||||
// try to load it with higher precision.
|
||||
|
||||
bool use_cmyk = cinfo.jpeg_color_space == JCS_CMYK ||
|
||||
cinfo.jpeg_color_space == JCS_YCCK;
|
||||
@@ -1612,11 +1406,16 @@ load_libjpeg_enhanced(
|
||||
{
|
||||
// Go for the maximum quality setting.
|
||||
jpegqs_control_t opts = {
|
||||
.flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV | JPEGQS_UPSAMPLE_UV,
|
||||
.flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV,
|
||||
.threads = g_get_num_processors(),
|
||||
.niter = 3,
|
||||
};
|
||||
|
||||
// Waiting for https://github.com/ilyakurdyukov/jpeg-quantsmooth/issues/28
|
||||
#if LIBJPEG_TURBO_VERSION_NUMBER < 2001090
|
||||
opts.flags |= JPEGQS_UPSAMPLE_UV;
|
||||
#endif
|
||||
|
||||
(void) jpegqs_start_decompress(cinfo, &opts);
|
||||
while (cinfo->output_scanline < cinfo->output_height)
|
||||
(void) jpeg_read_scanlines(cinfo, lines + cinfo->output_scanline,
|
||||
@@ -1625,7 +1424,7 @@ load_libjpeg_enhanced(
|
||||
}
|
||||
|
||||
#else
|
||||
#define load_libjpeg_enhanced libjpeg_turbo_load_simple
|
||||
#define load_libjpeg_enhanced load_libjpeg_simple
|
||||
#endif
|
||||
|
||||
static FivIoImage *
|
||||
@@ -1732,10 +1531,8 @@ load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool is_opaque = (info->bgcolor & 0xFF) == 0xFF;
|
||||
uint64_t area = info->canvas_width * info->canvas_height;
|
||||
FivIoImage *image = fiv_io_image_new(
|
||||
is_opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32,
|
||||
FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_RGB24,
|
||||
info->canvas_width, info->canvas_height);
|
||||
if (!image) {
|
||||
set_error(error, "image allocation failure");
|
||||
@@ -1746,11 +1543,20 @@ load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
|
||||
if (G_BYTE_ORDER == G_LITTLE_ENDIAN) {
|
||||
memcpy(dst, buf, area * sizeof *dst);
|
||||
} else {
|
||||
uint32_t *src = (uint32_t *) buf;
|
||||
for (uint64_t i = 0; i < area; i++)
|
||||
*dst++ = GUINT32_FROM_LE(*src++);
|
||||
const uint32_t *src = (const uint32_t *) buf;
|
||||
for (uint64_t i = 0; i < area; i++) {
|
||||
uint32_t value = *src++;
|
||||
*dst++ = GUINT32_FROM_LE(value);
|
||||
}
|
||||
}
|
||||
|
||||
// info->bgcolor is not reliable.
|
||||
for (const uint32_t *p = dst, *end = dst + area; p < end; p++)
|
||||
if ((~*p & 0xff000000)) {
|
||||
image->format = CAIRO_FORMAT_ARGB32;
|
||||
break;
|
||||
}
|
||||
|
||||
// This API is confusing and awkward.
|
||||
image->frame_duration = timestamp - *last_timestamp;
|
||||
*last_timestamp = timestamp;
|
||||
@@ -1872,7 +1678,8 @@ open_libwebp(
|
||||
|
||||
WebPDemuxDelete(demux);
|
||||
if (ctx->screen_profile)
|
||||
fiv_io_profile_argb32_premultiply_page(result, ctx->screen_profile);
|
||||
fiv_io_cmm_argb32_premultiply_page(
|
||||
ctx->cmm, result, ctx->screen_profile);
|
||||
|
||||
fail:
|
||||
WebPFreeDecBuffer(&config.output);
|
||||
@@ -2109,6 +1916,9 @@ load_tiff_ep(
|
||||
orientation >= 1 && orientation <= 8) {
|
||||
image->orientation = orientation;
|
||||
}
|
||||
|
||||
// XXX: AdobeRGB Nikon NEFs can only be distinguished by a ColorSpace tag
|
||||
// from within their MakerNote.
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -2251,7 +2061,7 @@ open_libraw(
|
||||
|
||||
out:
|
||||
libraw_close(iprc);
|
||||
return fiv_io_profile_finalize(result, ctx->screen_profile);
|
||||
return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile);
|
||||
}
|
||||
|
||||
#endif // HAVE_LIBRAW ---------------------------------------------------------
|
||||
@@ -2273,8 +2083,8 @@ load_resvg_destroy(FivIoRenderClosure *closure)
|
||||
}
|
||||
|
||||
static FivIoImage *
|
||||
load_resvg_render_internal(FivIoRenderClosureResvg *self,
|
||||
double scale, FivIoProfile target, GError **error)
|
||||
load_resvg_render_internal(FivIoRenderClosureResvg *self, double scale,
|
||||
FivIoCmm *cmm, FivIoProfile *target, GError **error)
|
||||
{
|
||||
double w = ceil(self->width * scale), h = ceil(self->height * scale);
|
||||
if (w > SHRT_MAX || h > SHRT_MAX) {
|
||||
@@ -2289,25 +2099,30 @@ load_resvg_render_internal(FivIoRenderClosureResvg *self,
|
||||
}
|
||||
|
||||
uint32_t *pixels = (uint32_t *) image->data;
|
||||
#if RESVG_MAJOR_VERSION == 0 && RESVG_MINOR_VERSION < 33
|
||||
resvg_fit_to fit_to = {
|
||||
scale == 1 ? RESVG_FIT_TO_TYPE_ORIGINAL : RESVG_FIT_TO_TYPE_ZOOM,
|
||||
scale};
|
||||
resvg_render(self->tree, fit_to, resvg_transform_identity(),
|
||||
image->width, image->height, (char *) pixels);
|
||||
#else
|
||||
resvg_render(self->tree, (resvg_transform) {.a = scale, .d = scale},
|
||||
image->width, image->height, (char *) pixels);
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < w * h; i++) {
|
||||
uint32_t rgba = g_ntohl(pixels[i]);
|
||||
pixels[i] = rgba << 24 | rgba >> 8;
|
||||
}
|
||||
return fiv_io_profile_finalize(image, target);
|
||||
return fiv_io_cmm_finish(cmm, image, target);
|
||||
}
|
||||
|
||||
static FivIoImage *
|
||||
load_resvg_render(FivIoRenderClosure *closure, double scale)
|
||||
load_resvg_render(FivIoRenderClosure *closure,
|
||||
FivIoCmm *cmm, FivIoProfile *target, double scale)
|
||||
{
|
||||
FivIoRenderClosureResvg *self = (FivIoRenderClosureResvg *) closure;
|
||||
// TODO(p): Somehow get the target colour management profile.
|
||||
return load_resvg_render_internal(self, scale, NULL, NULL);
|
||||
return load_resvg_render_internal(self, scale, cmm, target, NULL);
|
||||
}
|
||||
|
||||
static const char *
|
||||
@@ -2365,8 +2180,8 @@ open_resvg(
|
||||
closure->width = size.width;
|
||||
closure->height = size.height;
|
||||
|
||||
FivIoImage *image =
|
||||
load_resvg_render_internal(closure, 1., ctx->screen_profile, error);
|
||||
FivIoImage *image = load_resvg_render_internal(
|
||||
closure, 1., ctx->cmm, ctx->screen_profile, error);
|
||||
if (!image) {
|
||||
load_resvg_destroy(&closure->parent);
|
||||
return NULL;
|
||||
@@ -2396,7 +2211,7 @@ load_librsvg_destroy(FivIoRenderClosure *closure)
|
||||
|
||||
static FivIoImage *
|
||||
load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
|
||||
FivIoProfile target, GError **error)
|
||||
FivIoCmm *cmm, FivIoProfile *target, GError **error)
|
||||
{
|
||||
RsvgRectangle viewport = {.x = 0, .y = 0,
|
||||
.width = self->width * scale, .height = self->height * scale};
|
||||
@@ -2410,10 +2225,11 @@ load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
|
||||
cairo_surface_t *surface = fiv_io_image_to_surface_noref(image);
|
||||
cairo_t *cr = cairo_create(surface);
|
||||
cairo_surface_destroy(surface);
|
||||
(void) rsvg_handle_render_document(self->handle, cr, &viewport, error);
|
||||
gboolean success =
|
||||
rsvg_handle_render_document(self->handle, cr, &viewport, error);
|
||||
cairo_status_t status = cairo_status(cr);
|
||||
cairo_destroy(cr);
|
||||
if (error) {
|
||||
if (!success) {
|
||||
fiv_io_image_unref(image);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2422,15 +2238,15 @@ load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
|
||||
fiv_io_image_unref(image);
|
||||
return NULL;
|
||||
}
|
||||
return fiv_io_profile_finalize(image, target);
|
||||
return fiv_io_cmm_finish(cmm, image, target);
|
||||
}
|
||||
|
||||
static FivIoImage *
|
||||
load_librsvg_render(FivIoRenderClosure *closure, double scale)
|
||||
load_librsvg_render(FivIoRenderClosure *closure,
|
||||
FivIoCmm *cmm, FivIoProfile *target, double scale)
|
||||
{
|
||||
FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure;
|
||||
// TODO(p): Somehow get the target colour management profile.
|
||||
return load_librsvg_render_internal(self, scale, NULL, NULL);
|
||||
return load_librsvg_render_internal(self, scale, cmm, target, NULL);
|
||||
}
|
||||
|
||||
static FivIoImage *
|
||||
@@ -2479,15 +2295,15 @@ open_librsvg(
|
||||
|
||||
// librsvg rasterizes filters, so rendering to a recording Cairo surface
|
||||
// has been abandoned.
|
||||
FivIoImage *image =
|
||||
load_librsvg_render_internal(closure, 1., ctx->screen_profile, error);
|
||||
FivIoImage *image = load_librsvg_render_internal(
|
||||
closure, 1., ctx->cmm, ctx->screen_profile, error);
|
||||
if (!image) {
|
||||
load_librsvg_destroy(&closure->parent);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
image->render = &closure->parent;
|
||||
return fiv_io_profile_finalize(image, ctx->screen_profile);
|
||||
return image;
|
||||
}
|
||||
|
||||
#endif // HAVE_LIBRSVG --------------------------------------------------------
|
||||
@@ -2796,7 +2612,7 @@ open_libheif(
|
||||
g_free(ids);
|
||||
fail_read:
|
||||
heif_context_free(ctx);
|
||||
return fiv_io_profile_finalize(result, ioctx->screen_profile);
|
||||
return fiv_io_cmm_finish(ioctx->cmm, result, ioctx->screen_profile);
|
||||
}
|
||||
|
||||
#endif // HAVE_LIBHEIF --------------------------------------------------------
|
||||
@@ -3027,7 +2843,7 @@ fail:
|
||||
TIFFSetWarningHandlerExt(whe);
|
||||
TIFFSetErrorHandler(eh);
|
||||
TIFFSetWarningHandler(wh);
|
||||
return fiv_io_profile_finalize(result, ctx->screen_profile);
|
||||
return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile);
|
||||
}
|
||||
|
||||
#endif // HAVE_LIBTIFF --------------------------------------------------------
|
||||
@@ -3122,9 +2938,10 @@ open_gdkpixbuf(
|
||||
|
||||
g_object_unref(pixbuf);
|
||||
if (custom_argb32)
|
||||
fiv_io_profile_argb32_premultiply_page(image, ctx->screen_profile);
|
||||
fiv_io_cmm_argb32_premultiply_page(
|
||||
ctx->cmm, image, ctx->screen_profile);
|
||||
else
|
||||
image = fiv_io_profile_finalize(image, ctx->screen_profile);
|
||||
image = fiv_io_cmm_finish(ctx->cmm, image, ctx->screen_profile);
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -3577,7 +3394,7 @@ set_metadata(WebPMux *mux, const char *fourcc, GBytes *data)
|
||||
}
|
||||
|
||||
gboolean
|
||||
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile target,
|
||||
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile *target,
|
||||
const char *path, GError **error)
|
||||
{
|
||||
g_return_val_if_fail(page != NULL, FALSE);
|
||||
@@ -3641,34 +3458,40 @@ fiv_io_orientation_apply(const FivIoImage *image,
|
||||
FivIoOrientation orientation, double *width, double *height)
|
||||
{
|
||||
fiv_io_orientation_dimensions(image, orientation, width, height);
|
||||
return fiv_io_orientation_matrix(orientation, *width, *height);
|
||||
}
|
||||
|
||||
cairo_matrix_t
|
||||
fiv_io_orientation_matrix(
|
||||
FivIoOrientation orientation, double width, double height)
|
||||
{
|
||||
cairo_matrix_t matrix = {};
|
||||
cairo_matrix_init_identity(&matrix);
|
||||
switch (orientation) {
|
||||
case FivIoOrientation90:
|
||||
cairo_matrix_rotate(&matrix, -M_PI_2);
|
||||
cairo_matrix_translate(&matrix, -*width, 0);
|
||||
cairo_matrix_translate(&matrix, -width, 0);
|
||||
break;
|
||||
case FivIoOrientation180:
|
||||
cairo_matrix_scale(&matrix, -1, -1);
|
||||
cairo_matrix_translate(&matrix, -*width, -*height);
|
||||
cairo_matrix_translate(&matrix, -width, -height);
|
||||
break;
|
||||
case FivIoOrientation270:
|
||||
cairo_matrix_rotate(&matrix, +M_PI_2);
|
||||
cairo_matrix_translate(&matrix, 0, -*height);
|
||||
cairo_matrix_translate(&matrix, 0, -height);
|
||||
break;
|
||||
case FivIoOrientationMirror0:
|
||||
cairo_matrix_scale(&matrix, -1, +1);
|
||||
cairo_matrix_translate(&matrix, -*width, 0);
|
||||
cairo_matrix_translate(&matrix, -width, 0);
|
||||
break;
|
||||
case FivIoOrientationMirror90:
|
||||
cairo_matrix_rotate(&matrix, +M_PI_2);
|
||||
cairo_matrix_scale(&matrix, -1, +1);
|
||||
cairo_matrix_translate(&matrix, -*width, -*height);
|
||||
cairo_matrix_translate(&matrix, -width, -height);
|
||||
break;
|
||||
case FivIoOrientationMirror180:
|
||||
cairo_matrix_scale(&matrix, +1, -1);
|
||||
cairo_matrix_translate(&matrix, 0, -*height);
|
||||
cairo_matrix_translate(&matrix, 0, -height);
|
||||
break;
|
||||
case FivIoOrientationMirror270:
|
||||
cairo_matrix_rotate(&matrix, -M_PI_2);
|
||||
|
||||
69
fiv-io.h
69
fiv-io.h
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-io.h: image operations
|
||||
//
|
||||
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -22,16 +22,53 @@
|
||||
#include <glib.h>
|
||||
#include <webp/encode.h> // WebPConfig
|
||||
|
||||
typedef enum _FivIoOrientation FivIoOrientation;
|
||||
typedef struct _FivIoRenderClosure FivIoRenderClosure;
|
||||
typedef struct _FivIoImage FivIoImage;
|
||||
typedef struct _FivIoProfile FivIoProfile;
|
||||
|
||||
// --- Colour management -------------------------------------------------------
|
||||
// Note that without a CMM, all FivIoCmm and FivIoProfile will be returned NULL.
|
||||
|
||||
// TODO(p): Make it also possible to use Skia's skcms.
|
||||
typedef void *FivIoProfile;
|
||||
FivIoProfile fiv_io_profile_new(const void *data, size_t len);
|
||||
FivIoProfile fiv_io_profile_new_sRGB(void);
|
||||
void fiv_io_profile_free(FivIoProfile self);
|
||||
GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
|
||||
void fiv_io_profile_free(FivIoProfile *self);
|
||||
|
||||
// From libwebp, verified to exactly match [x * a / 255].
|
||||
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
#define FIV_TYPE_IO_CMM (fiv_io_cmm_get_type())
|
||||
G_DECLARE_FINAL_TYPE(FivIoCmm, fiv_io_cmm, FIV, IO_CMM, GObject)
|
||||
|
||||
FivIoCmm *fiv_io_cmm_get_default(void);
|
||||
|
||||
FivIoProfile *fiv_io_cmm_get_profile(
|
||||
FivIoCmm *self, const void *data, size_t len);
|
||||
FivIoProfile *fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes);
|
||||
FivIoProfile *fiv_io_cmm_get_profile_sRGB(FivIoCmm *self);
|
||||
FivIoProfile *fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma);
|
||||
FivIoProfile *fiv_io_cmm_get_profile_parametric(
|
||||
FivIoCmm *self, double gamma, double whitepoint[2], double primaries[6]);
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
void fiv_io_premultiply_argb32(FivIoImage *image);
|
||||
|
||||
void fiv_io_cmm_cmyk(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
|
||||
void fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
|
||||
int w, int h, FivIoProfile *source, FivIoProfile *target);
|
||||
|
||||
void fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
|
||||
#define fiv_io_cmm_argb32_premultiply_page(cmm, page, target) \
|
||||
fiv_io_cmm_page((cmm), (page), (target), fiv_io_cmm_argb32_premultiply)
|
||||
|
||||
void fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
|
||||
void (*frame_cb) (FivIoCmm *,
|
||||
FivIoImage *, FivIoProfile *, FivIoProfile *));
|
||||
void fiv_io_cmm_any(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
|
||||
FivIoImage *fiv_io_cmm_finish(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *target);
|
||||
|
||||
// --- Loading -----------------------------------------------------------------
|
||||
|
||||
@@ -39,10 +76,6 @@ extern const char *fiv_io_supported_media_types[];
|
||||
|
||||
gchar **fiv_io_all_supported_media_types(void);
|
||||
|
||||
typedef enum _FivIoOrientation FivIoOrientation;
|
||||
typedef struct _FivIoRenderClosure FivIoRenderClosure;
|
||||
typedef struct _FivIoImage FivIoImage;
|
||||
|
||||
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
|
||||
enum _FivIoOrientation {
|
||||
FivIoOrientationUnknown = 0,
|
||||
@@ -56,9 +89,12 @@ enum _FivIoOrientation {
|
||||
FivIoOrientation270 = 8
|
||||
};
|
||||
|
||||
// TODO(p): Maybe make FivIoProfile a referencable type,
|
||||
// then loaders could store it in their closures.
|
||||
struct _FivIoRenderClosure {
|
||||
/// The rendering is allowed to fail, returning NULL.
|
||||
FivIoImage *(*render)(FivIoRenderClosure *, double scale);
|
||||
FivIoImage *(*render)(
|
||||
FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
|
||||
void (*destroy)(FivIoRenderClosure *);
|
||||
};
|
||||
|
||||
@@ -129,7 +165,8 @@ cairo_surface_t *fiv_io_image_to_surface_noref(const FivIoImage *image);
|
||||
|
||||
typedef struct {
|
||||
const char *uri; ///< Source URI
|
||||
FivIoProfile screen_profile; ///< Target colour space or NULL
|
||||
FivIoCmm *cmm; ///< Colour management module or NULL
|
||||
FivIoProfile *screen_profile; ///< Target colour space or NULL
|
||||
int screen_dpi; ///< Target DPI
|
||||
gboolean enhance; ///< Enhance JPEG (currently)
|
||||
gboolean first_frame_only; ///< Only interested in the 1st frame
|
||||
@@ -148,6 +185,8 @@ FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error);
|
||||
/// and its target dimensions.
|
||||
cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image,
|
||||
FivIoOrientation orientation, double *width, double *height);
|
||||
cairo_matrix_t fiv_io_orientation_matrix(
|
||||
FivIoOrientation orientation, double width, double height);
|
||||
void fiv_io_orientation_dimensions(const FivIoImage *image,
|
||||
FivIoOrientation orientation, double *width, double *height);
|
||||
|
||||
@@ -177,4 +216,4 @@ unsigned char *fiv_io_encode_webp(
|
||||
/// Saves the page as a lossless WebP still picture or animation.
|
||||
/// If no exact frame is specified, this potentially creates an animation.
|
||||
gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
|
||||
FivIoProfile target, const char *path, GError **error);
|
||||
FivIoProfile *target, const char *path, GError **error);
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
#include <gtk/gtk.h>
|
||||
#include <turbojpeg.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
// --- Utilities ---------------------------------------------------------------
|
||||
|
||||
@@ -5,5 +5,5 @@ if [ "$#" -ne 2 ]; then
|
||||
fi
|
||||
|
||||
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
|
||||
| curl --silent --show-error --upload-file - https://transfer.sh/image \
|
||||
| jq --slurp --raw-input --raw-output @uri)"
|
||||
| curl --silent --show-error --form 'files[]=@-' https://uguu.se/upload \
|
||||
| jq --raw-output '.files[] | .url | @uri')"
|
||||
|
||||
@@ -433,7 +433,10 @@ complete_path(GFile *location, GtkListStore *model)
|
||||
!info)
|
||||
break;
|
||||
|
||||
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY ||
|
||||
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY)
|
||||
continue;
|
||||
if (g_file_info_has_attribute(info,
|
||||
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
|
||||
g_file_info_get_is_hidden(info))
|
||||
continue;
|
||||
|
||||
|
||||
@@ -100,7 +100,16 @@ mark_thumbnail_lq(cairo_surface_t *surface)
|
||||
static gchar *
|
||||
fiv_thumbnail_get_root(void)
|
||||
{
|
||||
#ifdef G_OS_WIN32
|
||||
// We can do better than GLib with FOLDERID_InternetCache,
|
||||
// and we don't want to place .cache directly in the user's home.
|
||||
// TODO(p): Register this thumbnail path using the installer:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/lwef/disk-cleanup
|
||||
gchar *cache_dir =
|
||||
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
|
||||
#else
|
||||
gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
|
||||
#endif
|
||||
gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL);
|
||||
g_free(cache_dir);
|
||||
return thumbnails_dir;
|
||||
@@ -128,9 +137,12 @@ might_be_a_thumbnail(const char *path_or_uri)
|
||||
static FivIoImage *
|
||||
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
|
||||
{
|
||||
FivIoCmm *cmm = fiv_io_cmm_get_default();
|
||||
FivIoOpenContext ctx = {
|
||||
.uri = g_file_get_uri(target),
|
||||
.screen_profile = fiv_io_profile_new_sRGB(),
|
||||
// Remember to synchronize changes with adjust_thumbnail().
|
||||
.cmm = cmm,
|
||||
.screen_profile = fiv_io_cmm_get_profile_sRGB(cmm),
|
||||
.screen_dpi = 96,
|
||||
.first_frame_only = TRUE,
|
||||
// Only using this array as a redirect.
|
||||
@@ -171,8 +183,14 @@ adjust_thumbnail(FivIoImage *thumbnail, double row_height)
|
||||
// Vector images should not have orientation, this should handle them all.
|
||||
FivIoRenderClosure *closure = thumbnail->render;
|
||||
if (closure && orientation <= FivIoOrientation0) {
|
||||
// Remember to synchronize changes with render().
|
||||
FivIoCmm *cmm = fiv_io_cmm_get_default();
|
||||
FivIoProfile *screen_profile = fiv_io_cmm_get_profile_sRGB(cmm);
|
||||
// This API doesn't accept non-uniform scaling; prefer a vertical fit.
|
||||
FivIoImage *scaled = closure->render(closure, scale_y);
|
||||
FivIoImage *scaled =
|
||||
closure->render(closure, cmm, screen_profile, scale_y);
|
||||
if (screen_profile)
|
||||
fiv_io_profile_free(screen_profile);
|
||||
if (scaled)
|
||||
return scaled;
|
||||
}
|
||||
@@ -374,7 +392,7 @@ extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
|
||||
// The main image's "flip" often matches up, but sometimes doesn't, e.g.:
|
||||
// - Phase One/H 25/H25_Outdoor_.IIQ
|
||||
// - Phase One/H 25/H25_IT8.7-2_Card.TIF
|
||||
*flip = iprc->sizes.flip
|
||||
*flip = iprc->sizes.flip;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -417,7 +435,7 @@ extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
|
||||
|
||||
guint32 *out = (guint32 *) I->data;
|
||||
const unsigned char *in = image->data;
|
||||
for (guint64 i = 0; i < image->width * image->height; in += 3)
|
||||
for (guint64 i = 0; i < (guint64) image->width * image->height; in += 3)
|
||||
out[i++] = in[0] << 16 | in[1] << 8 | in[2];
|
||||
|
||||
I->orientation = extract_libraw_unflip(flip);
|
||||
|
||||
487
fiv-view.c
487
fiv-view.c
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-view.c: image viewing widget
|
||||
//
|
||||
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <epoxy/gl.h>
|
||||
#include <gtk/gtk.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
@@ -78,11 +79,15 @@ struct _FivView {
|
||||
double drag_start[2]; ///< Adjustment values for drag origin
|
||||
|
||||
FivIoImage *enhance_swap; ///< Quick swap in/out
|
||||
FivIoProfile screen_cms_profile; ///< Target colour profile for widget
|
||||
FivIoProfile *screen_cms_profile; ///< Target colour profile for widget
|
||||
|
||||
int remaining_loops; ///< Greater than zero if limited
|
||||
gint64 frame_time; ///< Current frame's start, µs precision
|
||||
gulong frame_update_connection; ///< GdkFrameClock::update
|
||||
|
||||
GdkGLContext *gl_context; ///< OpenGL context
|
||||
bool gl_initialized; ///< Objects have been created
|
||||
GLuint gl_program; ///< Linked render program
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0,
|
||||
@@ -161,6 +166,147 @@ enum {
|
||||
// Globals are, sadly, the canonical way of storing signal numbers.
|
||||
static guint view_signals[LAST_SIGNAL];
|
||||
|
||||
// --- OpenGL ------------------------------------------------------------------
|
||||
// While GTK+ 3 technically still supports legacy desktop OpenGL 2.0[1],
|
||||
// we will pick the 3.3 core profile, which is fairly old by now.
|
||||
// It doesn't seem to make any sense to go below 3.2.
|
||||
//
|
||||
// [1] https://stackoverflow.com/a/37923507/76313
|
||||
//
|
||||
// OpenGL ES
|
||||
//
|
||||
// Currently, we do not support OpenGL ES at all--it needs its own shaders
|
||||
// (if only because of different #version statements), and also further analysis
|
||||
// as to what is our minimum version requirement. While GTK+ 3 can again go
|
||||
// down as low as OpenGL ES 2.0, this might be too much of a hassle to support.
|
||||
//
|
||||
// ES can be forced via GDK_GL=gles, if gdk_gl_context_set_required_version()
|
||||
// doesn't stand in the way.
|
||||
//
|
||||
// Let's not forget that this is a desktop image viewer first and foremost.
|
||||
|
||||
static const char *
|
||||
gl_error_string(GLenum err)
|
||||
{
|
||||
switch (err) {
|
||||
case GL_NO_ERROR:
|
||||
return "no error";
|
||||
case GL_CONTEXT_LOST:
|
||||
return "context lost";
|
||||
case GL_INVALID_ENUM:
|
||||
return "invalid enum";
|
||||
case GL_INVALID_VALUE:
|
||||
return "invalid value";
|
||||
case GL_INVALID_OPERATION:
|
||||
return "invalid operation";
|
||||
case GL_INVALID_FRAMEBUFFER_OPERATION:
|
||||
return "invalid framebuffer operation";
|
||||
case GL_OUT_OF_MEMORY:
|
||||
return "out of memory";
|
||||
case GL_STACK_UNDERFLOW:
|
||||
return "stack underflow";
|
||||
case GL_STACK_OVERFLOW:
|
||||
return "stack overflow";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static const char *gl_vertex =
|
||||
"#version 330\n"
|
||||
"layout(location = 0) in vec4 position;\n"
|
||||
"out vec2 coordinates;\n"
|
||||
"void main() {\n"
|
||||
"\tcoordinates = position.zw;\n"
|
||||
"\tgl_Position = vec4(position.xy, 0., 1.);\n"
|
||||
"}\n";
|
||||
|
||||
static const char *gl_fragment =
|
||||
"#version 330\n"
|
||||
"in vec2 coordinates;\n"
|
||||
"layout(location = 0) out vec4 color;\n"
|
||||
"uniform sampler2D picture;\n"
|
||||
"uniform bool checkerboard;\n"
|
||||
"\n"
|
||||
"vec3 checker() {\n"
|
||||
"\tvec2 xy = gl_FragCoord.xy / 20.;\n"
|
||||
"\tif (checkerboard && (int(floor(xy.x) + floor(xy.y)) & 1) == 0)\n"
|
||||
"\t\treturn vec3(0.98);\n"
|
||||
"\telse\n"
|
||||
"\t\treturn vec3(1.00);\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"void main() {\n"
|
||||
"\tvec3 c = checker();\n"
|
||||
"\tvec4 t = texture(picture, coordinates);\n"
|
||||
"\t// Premultiplied blending with a solid background.\n"
|
||||
"\t// XXX: This is only correct for linear components.\n"
|
||||
"\tcolor = vec4(c * (1. - t.a) + t.rgb, 1.);\n"
|
||||
"}\n";
|
||||
|
||||
static GLuint
|
||||
gl_make_shader(int type, const char *glsl)
|
||||
{
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &glsl, NULL);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint status = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (!status) {
|
||||
GLint len = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
|
||||
|
||||
GLchar *buffer = g_malloc0(len + 1);
|
||||
glGetShaderInfoLog(shader, len, NULL, buffer);
|
||||
g_warning("GL shader compilation failed: %s", buffer);
|
||||
g_free(buffer);
|
||||
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
static GLuint
|
||||
gl_make_program(void)
|
||||
{
|
||||
GLuint vertex = gl_make_shader(GL_VERTEX_SHADER, gl_vertex);
|
||||
GLuint fragment = gl_make_shader(GL_FRAGMENT_SHADER, gl_fragment);
|
||||
if (!vertex || !fragment) {
|
||||
glDeleteShader(vertex);
|
||||
glDeleteShader(fragment);
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertex);
|
||||
glAttachShader(program, fragment);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vertex);
|
||||
glDeleteShader(fragment);
|
||||
|
||||
GLint status = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (!status) {
|
||||
GLint len = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
|
||||
|
||||
GLchar *buffer = g_malloc0(len + 1);
|
||||
glGetProgramInfoLog(program, len, NULL, buffer);
|
||||
g_warning("GL program linking failed: %s", buffer);
|
||||
g_free(buffer);
|
||||
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static void
|
||||
on_adjustment_value_changed(
|
||||
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
|
||||
@@ -198,12 +344,14 @@ update_adjustments(FivView *self)
|
||||
|
||||
if (self->hadjustment) {
|
||||
gtk_adjustment_configure(self->hadjustment,
|
||||
gtk_adjustment_get_value(self->hadjustment), 0, dw,
|
||||
gtk_adjustment_get_value(self->hadjustment),
|
||||
0, MAX(dw, alloc.width),
|
||||
alloc.width * 0.1, alloc.width * 0.9, alloc.width);
|
||||
}
|
||||
if (self->vadjustment) {
|
||||
gtk_adjustment_configure(self->vadjustment,
|
||||
gtk_adjustment_get_value(self->vadjustment), 0, dh,
|
||||
gtk_adjustment_get_value(self->vadjustment),
|
||||
0, MAX(dh, alloc.height),
|
||||
alloc.height * 0.1, alloc.height * 0.9, alloc.height);
|
||||
}
|
||||
}
|
||||
@@ -407,9 +555,16 @@ prescale_page(FivView *self)
|
||||
// TODO(p): Restart the animation. No vector formats currently animate.
|
||||
g_return_if_fail(!self->frame_update_connection);
|
||||
|
||||
// Optimization, taking into account the workaround in set_scale().
|
||||
if (!self->page_scaled &&
|
||||
(self->scale == 1 || self->scale == 0.999999999999999))
|
||||
return;
|
||||
|
||||
// If it fails, the previous frame pointer may become invalid.
|
||||
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
|
||||
self->frame = self->page_scaled = closure->render(closure, self->scale);
|
||||
self->frame = self->page_scaled = closure->render(closure,
|
||||
self->enable_cms ? fiv_io_cmm_get_default() : NULL,
|
||||
self->enable_cms ? self->screen_cms_profile : NULL, self->scale);
|
||||
if (!self->page_scaled)
|
||||
self->frame = self->page;
|
||||
}
|
||||
@@ -453,6 +608,27 @@ out:
|
||||
//
|
||||
// Note that Wayland does not have any appropriate protocol, as of writing:
|
||||
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
|
||||
static FivIoProfile *
|
||||
monitor_cms_profile(GdkWindow *root, int num)
|
||||
{
|
||||
char atom[32] = "";
|
||||
g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num);
|
||||
|
||||
// Sadly, there is no nice GTK+/GDK mechanism to watch this for changes.
|
||||
int format = 0, length = 0;
|
||||
GdkAtom type = GDK_NONE;
|
||||
guchar *data = NULL;
|
||||
FivIoProfile *result = NULL;
|
||||
if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
|
||||
8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
|
||||
if (format == 8 && length > 0)
|
||||
result = fiv_io_cmm_get_profile(
|
||||
fiv_io_cmm_get_default(), data, length);
|
||||
g_free(data);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
reload_screen_cms_profile(FivView *self, GdkWindow *window)
|
||||
{
|
||||
@@ -470,7 +646,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
|
||||
gchar *data = NULL;
|
||||
gsize length = 0;
|
||||
if (g_file_get_contents(path, &data, &length, NULL))
|
||||
self->screen_cms_profile = fiv_io_profile_new(data, length);
|
||||
self->screen_cms_profile = fiv_io_cmm_get_profile(
|
||||
fiv_io_cmm_get_default(), data, length);
|
||||
g_free(data);
|
||||
}
|
||||
g_free(path);
|
||||
@@ -482,6 +659,7 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
|
||||
|
||||
GdkDisplay *display = gdk_window_get_display(window);
|
||||
GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
|
||||
GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
|
||||
|
||||
int num = -1;
|
||||
for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; )
|
||||
@@ -490,24 +668,14 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
|
||||
if (num < 0)
|
||||
goto out;
|
||||
|
||||
char atom[32] = "";
|
||||
g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num);
|
||||
|
||||
// Sadly, there is no nice GTK+/GDK mechanism to watch this for changes.
|
||||
int format = 0, length = 0;
|
||||
GdkAtom type = GDK_NONE;
|
||||
guchar *data = NULL;
|
||||
GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
|
||||
if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
|
||||
8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
|
||||
if (format == 8 && length > 0)
|
||||
self->screen_cms_profile = fiv_io_profile_new(data, length);
|
||||
g_free(data);
|
||||
}
|
||||
// Cater to xiccd limitations (agalakhov/xiccd#33).
|
||||
if (!(self->screen_cms_profile = monitor_cms_profile(root, num)) && num)
|
||||
self->screen_cms_profile = monitor_cms_profile(root, 0);
|
||||
|
||||
out:
|
||||
if (!self->screen_cms_profile)
|
||||
self->screen_cms_profile = fiv_io_profile_new_sRGB();
|
||||
self->screen_cms_profile =
|
||||
fiv_io_cmm_get_profile_sRGB(fiv_io_cmm_get_default());
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -541,6 +709,9 @@ fiv_view_realize(GtkWidget *widget)
|
||||
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
|
||||
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
|
||||
|
||||
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
|
||||
gboolean opengl = g_settings_get_boolean(settings, "opengl");
|
||||
|
||||
// Without the following call, or the rendering mode set to "recording",
|
||||
// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
|
||||
// creates backing stores using cairo_content_t constants.
|
||||
@@ -550,20 +721,274 @@ fiv_view_realize(GtkWidget *widget)
|
||||
// Note that this disables double buffering, and sometimes causes artefacts,
|
||||
// see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
|
||||
//
|
||||
// If GTK+'s OpenGL integration fails to deliver, we need to use the window
|
||||
// directly, sidestepping the toolkit entirely.
|
||||
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
|
||||
// GTK+'s OpenGL integration is terrible, so we may need to use
|
||||
// the X11 subwindow directly, sidestepping the toolkit entirely.
|
||||
if (GDK_IS_X11_WINDOW(window) &&
|
||||
g_settings_get_boolean(settings, "native-view-window"))
|
||||
gdk_window_ensure_native(window);
|
||||
g_object_unref(settings);
|
||||
#endif // GDK_WINDOWING_X11
|
||||
g_object_unref(settings);
|
||||
|
||||
gtk_widget_register_window(widget, window);
|
||||
gtk_widget_set_window(widget, window);
|
||||
gtk_widget_set_realized(widget, TRUE);
|
||||
|
||||
reload_screen_cms_profile(FIV_VIEW(widget), window);
|
||||
|
||||
FivView *self = FIV_VIEW(widget);
|
||||
g_clear_object(&self->gl_context);
|
||||
if (!opengl)
|
||||
return;
|
||||
|
||||
GError *error = NULL;
|
||||
GdkGLContext *gl_context = gdk_window_create_gl_context(window, &error);
|
||||
if (!gl_context) {
|
||||
g_warning("GL: %s", error->message);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
|
||||
gdk_gl_context_set_use_es(gl_context, FALSE);
|
||||
gdk_gl_context_set_required_version(gl_context, 3, 3);
|
||||
gdk_gl_context_set_debug_enabled(gl_context, TRUE);
|
||||
|
||||
if (!gdk_gl_context_realize(gl_context, &error)) {
|
||||
g_warning("GL: %s", error->message);
|
||||
g_error_free(error);
|
||||
g_object_unref(gl_context);
|
||||
return;
|
||||
}
|
||||
|
||||
self->gl_context = gl_context;
|
||||
}
|
||||
|
||||
static void GLAPIENTRY
|
||||
gl_on_message(G_GNUC_UNUSED GLenum source, GLenum type, G_GNUC_UNUSED GLuint id,
|
||||
G_GNUC_UNUSED GLenum severity, G_GNUC_UNUSED GLsizei length,
|
||||
const GLchar *message, G_GNUC_UNUSED const void *user_data)
|
||||
{
|
||||
if (type == GL_DEBUG_TYPE_ERROR)
|
||||
g_warning("GL: error: %s", message);
|
||||
else
|
||||
g_debug("GL: %s", message);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_view_unrealize(GtkWidget *widget)
|
||||
{
|
||||
FivView *self = FIV_VIEW(widget);
|
||||
if (self->gl_context) {
|
||||
if (self->gl_initialized) {
|
||||
gdk_gl_context_make_current(self->gl_context);
|
||||
glDeleteProgram(self->gl_program);
|
||||
}
|
||||
if (self->gl_context == gdk_gl_context_get_current())
|
||||
gdk_gl_context_clear_current();
|
||||
|
||||
g_clear_object(&self->gl_context);
|
||||
}
|
||||
|
||||
GTK_WIDGET_CLASS(fiv_view_parent_class)->unrealize(widget);
|
||||
}
|
||||
|
||||
static bool
|
||||
gl_draw(FivView *self, cairo_t *cr)
|
||||
{
|
||||
gdk_gl_context_make_current(self->gl_context);
|
||||
|
||||
if (!self->gl_initialized) {
|
||||
GLuint program = gl_make_program();
|
||||
if (!program)
|
||||
return false;
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_BLEND);
|
||||
if (epoxy_has_gl_extension("GL_ARB_debug_output")) {
|
||||
glEnable(GL_DEBUG_OUTPUT);
|
||||
glDebugMessageCallback(gl_on_message, NULL);
|
||||
}
|
||||
|
||||
self->gl_program = program;
|
||||
self->gl_initialized = true;
|
||||
}
|
||||
|
||||
// This limit is always less than that of Cairo/pixman,
|
||||
// and we'd have to figure out tiling.
|
||||
GLint max = 0;
|
||||
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
|
||||
if (max < (GLint) self->frame->width ||
|
||||
max < (GLint) self->frame->height) {
|
||||
g_warning("OpenGL max. texture size is too small");
|
||||
return false;
|
||||
}
|
||||
|
||||
GtkAllocation allocation;
|
||||
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
|
||||
int dw = 0, dh = 0, dx = 0, dy = 0;
|
||||
get_display_dimensions(self, &dw, &dh);
|
||||
|
||||
int clipw = dw, cliph = dh;
|
||||
double x1 = 0., y1 = 0., x2 = 1., y2 = 1.;
|
||||
if (self->hadjustment)
|
||||
x1 = floor(gtk_adjustment_get_value(self->hadjustment)) / dw;
|
||||
if (self->vadjustment)
|
||||
y1 = floor(gtk_adjustment_get_value(self->vadjustment)) / dh;
|
||||
|
||||
if (dw <= allocation.width) {
|
||||
dx = round((allocation.width - dw) / 2.);
|
||||
} else {
|
||||
x2 = x1 + (double) allocation.width / dw;
|
||||
clipw = allocation.width;
|
||||
}
|
||||
|
||||
if (dh <= allocation.height) {
|
||||
dy = round((allocation.height - dh) / 2.);
|
||||
} else {
|
||||
y2 = y1 + (double) allocation.height / dh;
|
||||
cliph = allocation.height;
|
||||
}
|
||||
|
||||
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self));
|
||||
clipw *= scale;
|
||||
cliph *= scale;
|
||||
|
||||
enum { SRC, DEST };
|
||||
GLuint textures[2] = {};
|
||||
glGenTextures(2, textures);
|
||||
|
||||
// https://stackoverflow.com/questions/25157306 0..1
|
||||
// GL_TEXTURE_RECTANGLE seems kind-of useful
|
||||
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
if (self->filter) {
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
} else {
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
}
|
||||
|
||||
// GL_UNPACK_ALIGNMENT is initially 4, which is fine for these.
|
||||
// Texture swizzling is OpenGL 3.3.
|
||||
if (self->frame->format == CAIRO_FORMAT_ARGB32) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
||||
self->frame->width, self->frame->height,
|
||||
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
|
||||
} else if (self->frame->format == CAIRO_FORMAT_RGB24) {
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
||||
self->frame->width, self->frame->height,
|
||||
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
|
||||
} else if (self->frame->format == CAIRO_FORMAT_RGB30) {
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
||||
self->frame->width, self->frame->height,
|
||||
0, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV, self->frame->data);
|
||||
} else {
|
||||
g_warning("GL: unsupported bitmap format");
|
||||
}
|
||||
|
||||
// GtkGLArea creates textures like this.
|
||||
glBindTexture(GL_TEXTURE_2D, textures[DEST]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, clipw, cliph, 0, GL_BGRA,
|
||||
GL_UNSIGNED_BYTE, NULL);
|
||||
|
||||
glViewport(0, 0, clipw, cliph);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
|
||||
GLuint frame_buffer = 0;
|
||||
glGenFramebuffers(1, &frame_buffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);
|
||||
glFramebufferTexture2D(
|
||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[DEST], 0);
|
||||
|
||||
glClearColor(0., 0., 0., 1.);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE)
|
||||
g_warning("GL framebuffer status: %u", status);
|
||||
|
||||
glUseProgram(self->gl_program);
|
||||
GLint position_location = glGetAttribLocation(
|
||||
self->gl_program, "position");
|
||||
GLint picture_location = glGetUniformLocation(
|
||||
self->gl_program, "picture");
|
||||
GLint checkerboard_location = glGetUniformLocation(
|
||||
self->gl_program, "checkerboard");
|
||||
|
||||
glUniform1i(picture_location, 0);
|
||||
glUniform1i(checkerboard_location, self->checkerboard);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
|
||||
|
||||
// Note that the Y axis is flipped in the table.
|
||||
double vertices[][4] = {
|
||||
{-1., -1., x1, y2},
|
||||
{+1., -1., x2, y2},
|
||||
{+1., +1., x2, y1},
|
||||
{-1., +1., x1, y1},
|
||||
};
|
||||
|
||||
cairo_matrix_t matrix = fiv_io_orientation_matrix(self->orientation, 1, 1);
|
||||
cairo_matrix_transform_point(&matrix, &vertices[0][2], &vertices[0][3]);
|
||||
cairo_matrix_transform_point(&matrix, &vertices[1][2], &vertices[1][3]);
|
||||
cairo_matrix_transform_point(&matrix, &vertices[2][2], &vertices[2][3]);
|
||||
cairo_matrix_transform_point(&matrix, &vertices[3][2], &vertices[3][3]);
|
||||
|
||||
GLuint vertex_buffer = 0;
|
||||
glGenBuffers(1, &vertex_buffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
|
||||
glBindVertexArray(vao);
|
||||
glVertexAttribPointer(position_location,
|
||||
G_N_ELEMENTS(vertices[0]), GL_DOUBLE, GL_FALSE, sizeof vertices[0], 0);
|
||||
glEnableVertexAttribArray(position_location);
|
||||
glDrawArrays(GL_TRIANGLE_FAN, 0, G_N_ELEMENTS(vertices));
|
||||
glDisableVertexAttribArray(position_location);
|
||||
glBindVertexArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// XXX: Native GdkWindows send this to the software fallback path.
|
||||
// XXX: This only reliably alpha blends when using the software fallback,
|
||||
// such as with a native window, because 7237f5d in GTK+ 3 is a regression.
|
||||
// (Introduced in 3.24.39, reverted in 3.24.42.)
|
||||
//
|
||||
// We had to resort to rendering the checkerboard pattern in the shader.
|
||||
// Unfortunately, it is hard to retrieve the theme colours from CSS.
|
||||
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
|
||||
cairo_translate(cr, dx, dy);
|
||||
gdk_cairo_draw_from_gl(
|
||||
cr, window, textures[DEST], GL_TEXTURE, scale, 0, 0, clipw, cliph);
|
||||
gdk_gl_context_make_current(self->gl_context);
|
||||
|
||||
glDeleteBuffers(1, &vertex_buffer);
|
||||
glDeleteTextures(2, textures);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteFramebuffers(1, &frame_buffer);
|
||||
|
||||
// TODO(p): Possibly use this clue as a hint to use Cairo rendering.
|
||||
GLenum err = 0;
|
||||
while ((err = glGetError()) != GL_NO_ERROR) {
|
||||
const char *string = gl_error_string(err);
|
||||
if (string)
|
||||
g_warning("GL: error: %s", string);
|
||||
else
|
||||
g_warning("GL: error: %u", err);
|
||||
}
|
||||
|
||||
gdk_gl_context_clear_current();
|
||||
return true;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
@@ -580,8 +1005,10 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
|
||||
if (!self->image ||
|
||||
!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
|
||||
return TRUE;
|
||||
if (self->gl_context && gl_draw(self, cr))
|
||||
return TRUE;
|
||||
|
||||
int dw, dh;
|
||||
int dw = 0, dh = 0;
|
||||
get_display_dimensions(self, &dw, &dh);
|
||||
|
||||
double x = 0;
|
||||
@@ -885,6 +1312,10 @@ switch_page(FivView *self, FivIoImage *page)
|
||||
{
|
||||
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
|
||||
self->frame = self->page = page;
|
||||
|
||||
// XXX: When self->scale_to_fit is in effect,
|
||||
// this uses an old value that may no longer be appropriate,
|
||||
// resulting in wasted effort.
|
||||
prescale_page(self);
|
||||
|
||||
if (!self->page ||
|
||||
@@ -1095,7 +1526,7 @@ static gboolean
|
||||
save_as(FivView *self, FivIoImage *frame)
|
||||
{
|
||||
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
|
||||
FivIoProfile target = NULL;
|
||||
FivIoProfile *target = NULL;
|
||||
if (self->enable_cms && (target = self->screen_cms_profile)) {
|
||||
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
|
||||
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
|
||||
@@ -1271,6 +1702,7 @@ fiv_view_class_init(FivViewClass *klass)
|
||||
widget_class->map = fiv_view_map;
|
||||
widget_class->unmap = fiv_view_unmap;
|
||||
widget_class->realize = fiv_view_realize;
|
||||
widget_class->unrealize = fiv_view_unrealize;
|
||||
widget_class->draw = fiv_view_draw;
|
||||
widget_class->button_press_event = fiv_view_button_press_event;
|
||||
widget_class->scroll_event = fiv_view_scroll_event;
|
||||
@@ -1359,6 +1791,7 @@ open_without_swapping_in(FivView *self, const char *uri)
|
||||
{
|
||||
FivIoOpenContext ctx = {
|
||||
.uri = uri,
|
||||
.cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL,
|
||||
.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
|
||||
.screen_dpi = 96, // TODO(p): Try to retrieve it from the screen.
|
||||
.enhance = self->enhance,
|
||||
|
||||
360
fiv.c
360
fiv.c
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
|
||||
//
|
||||
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef G_OS_WIN32
|
||||
#include <io.h>
|
||||
@@ -43,11 +44,6 @@
|
||||
#include "fiv-thumbnail.h"
|
||||
#include "fiv-view.h"
|
||||
|
||||
#ifdef HAVE_LCMS2_FAST_FLOAT
|
||||
#include <lcms2.h>
|
||||
#include <lcms2_fast_float.h>
|
||||
#endif // HAVE_LCMS2_FAST_FLOAT
|
||||
|
||||
// --- Utilities ---------------------------------------------------------------
|
||||
|
||||
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
|
||||
@@ -100,16 +96,16 @@ struct key_section {
|
||||
static struct key help_keys_general[] = {
|
||||
{"F1", "Show help"},
|
||||
{"F10", "Open menu"},
|
||||
{"<Control>comma", "Preferences"},
|
||||
{"<Control>question", "Keyboard shortcuts"},
|
||||
{"q <Control>q", "Quit"},
|
||||
{"<Control>w", "Quit"},
|
||||
{"<Primary>comma", "Preferences"},
|
||||
{"<Primary>question", "Keyboard shortcuts"},
|
||||
{"q <Primary>q", "Quit"},
|
||||
{"<Primary>w", "Quit"},
|
||||
{}
|
||||
};
|
||||
|
||||
static struct key help_keys_navigation[] = {
|
||||
{"<Control>l", "Open location..."},
|
||||
{"<Control>n", "Open a new window"},
|
||||
{"<Primary>l", "Open location..."},
|
||||
{"<Primary>n", "Open a new window"},
|
||||
{"<Alt>Left", "Go back in history"},
|
||||
{"<Alt>Right", "Go forward in history"},
|
||||
{}
|
||||
@@ -458,7 +454,7 @@ show_about_dialog(GtkWidget *parent)
|
||||
|
||||
GtkWidget *website = gtk_label_new(NULL);
|
||||
gtk_label_set_selectable(GTK_LABEL(website), TRUE);
|
||||
const char *url = "https://git.janouch.name/p/" PROJECT_NAME;
|
||||
const char *url = PROJECT_URL;
|
||||
gchar *link = g_strdup_printf("<a href='%s'>%s</a>", url, url);
|
||||
gtk_label_set_markup(GTK_LABEL(website), link);
|
||||
g_free(link);
|
||||
@@ -604,6 +600,10 @@ show_preferences(GtkWidget *parent)
|
||||
int row = 0;
|
||||
gchar **keys = g_settings_schema_list_keys(schema);
|
||||
for (gchar **p = keys; *p; p++) {
|
||||
#ifndef GDK_WINDOWING_X11
|
||||
if (g_str_equal(*p, "native-view-window"))
|
||||
continue;
|
||||
#endif
|
||||
GSettingsSchemaKey *key = g_settings_schema_get_key(schema, *p);
|
||||
preferences_make_row(grid, &row, settings, key);
|
||||
g_settings_schema_key_unref(key);
|
||||
@@ -1457,92 +1457,17 @@ toggle_sunlight(void)
|
||||
g_object_set(settings, property, !set, NULL);
|
||||
}
|
||||
|
||||
// Cursor keys, e.g., simply cannot be bound through accelerators
|
||||
// (and GtkWidget::keynav-failed would arguably be an awful solution).
|
||||
//
|
||||
// GtkBindingSets can be added directly through GtkStyleContext,
|
||||
// but that would still require setting up action signals on the widget class,
|
||||
// which is extremely cumbersome. GtkWidget::move-focus has no return value,
|
||||
// so we can't override that and abort further handling.
|
||||
//
|
||||
// Therefore, bind directly to keypresses. Order can be fine-tuned with
|
||||
// g_signal_connect{,after}(), or overriding the handler and either tactically
|
||||
// chaining up or using gtk_window_propagate_key_event().
|
||||
static gboolean
|
||||
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
||||
G_GNUC_UNUSED gpointer data)
|
||||
{
|
||||
switch (event->state & gtk_accelerator_get_default_mod_mask()) {
|
||||
case GDK_MOD1_MASK | GDK_SHIFT_MASK:
|
||||
if (event->keyval == GDK_KEY_D)
|
||||
toggle_sunlight();
|
||||
break;
|
||||
case GDK_CONTROL_MASK:
|
||||
case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_h:
|
||||
// XXX: Command-H is already occupied on macOS.
|
||||
gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
|
||||
return TRUE;
|
||||
case GDK_KEY_l:
|
||||
fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
|
||||
return TRUE;
|
||||
case GDK_KEY_n:
|
||||
if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
|
||||
spawn_uri(g.uri);
|
||||
else
|
||||
spawn_uri(g.directory);
|
||||
return TRUE;
|
||||
case GDK_KEY_o:
|
||||
on_open();
|
||||
return TRUE;
|
||||
case GDK_KEY_q:
|
||||
case GDK_KEY_w:
|
||||
gtk_widget_destroy(g.window);
|
||||
return TRUE;
|
||||
|
||||
case GDK_KEY_question:
|
||||
show_help_shortcuts();
|
||||
return TRUE;
|
||||
case GDK_KEY_comma:
|
||||
show_preferences(g.window);
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
case GDK_MOD1_MASK:
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_Left:
|
||||
go_back();
|
||||
return TRUE;
|
||||
case GDK_KEY_Right:
|
||||
go_forward();
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
case GDK_SHIFT_MASK:
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_F1:
|
||||
show_about_dialog(g.window);
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
case 0:
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_BackSpace:
|
||||
go_back();
|
||||
return TRUE;
|
||||
case GDK_KEY_q:
|
||||
gtk_widget_destroy(g.window);
|
||||
return TRUE;
|
||||
case GDK_KEY_o:
|
||||
on_open();
|
||||
return TRUE;
|
||||
case GDK_KEY_F1:
|
||||
show_help_contents();
|
||||
return TRUE;
|
||||
case GDK_KEY_F11:
|
||||
case GDK_KEY_f:
|
||||
toggle_fullscreen();
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1557,8 +1482,15 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
||||
gtk_accelerator_parse(accelerator, &key, &mods);
|
||||
g_free(accelerator);
|
||||
|
||||
// TODO(p): See how Unity 7 behaves,
|
||||
// we might want to keep GtkApplicationWindow:show-menubar then.
|
||||
gboolean shell_shows_menubar = FALSE;
|
||||
(void) g_object_get(gtk_settings_get_default(),
|
||||
"gtk-shell-shows-menubar", &shell_shows_menubar, NULL);
|
||||
|
||||
guint mask = gtk_accelerator_get_default_mod_mask();
|
||||
if (key && event->keyval == key && (event->state & mask) == mods) {
|
||||
if (key && event->keyval == key && (event->state & mask) == mods &&
|
||||
!shell_shows_menubar) {
|
||||
gtk_widget_show(g.menu);
|
||||
|
||||
// _gtk_menu_shell_set_keyboard_mode() is private.
|
||||
@@ -1568,6 +1500,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Cursor keys, e.g., simply cannot be bound through accelerators
|
||||
// (and GtkWidget::keynav-failed would arguably be an awful solution).
|
||||
//
|
||||
// GtkBindingSets can be added directly through GtkStyleContext,
|
||||
// but that would still require setting up action signals on the widget class,
|
||||
// which is extremely cumbersome. GtkWidget::move-focus has no return value,
|
||||
// so we can't override that and abort further handling.
|
||||
//
|
||||
// Therefore, bind directly to keypresses. Order can be fine-tuned with
|
||||
// g_signal_connect{,after}(), or overriding the handler and either tactically
|
||||
// chaining up or using gtk_window_propagate_key_event().
|
||||
static gboolean
|
||||
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
||||
G_GNUC_UNUSED gpointer data)
|
||||
@@ -1746,6 +1689,9 @@ make_toolbar_radio(const char *label, const char *tooltip)
|
||||
GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
|
||||
gtk_widget_set_tooltip_text(button, tooltip);
|
||||
gtk_widget_set_focus_on_click(button, FALSE);
|
||||
gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(button), FALSE);
|
||||
gtk_style_context_add_class(
|
||||
gtk_widget_get_style_context(button), GTK_STYLE_CLASS_FLAT);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -1807,7 +1753,6 @@ make_browser_toolbar(void)
|
||||
gtk_radio_button_join_group(radio, last);
|
||||
last = radio;
|
||||
}
|
||||
|
||||
return browser_toolbar;
|
||||
}
|
||||
|
||||
@@ -2074,41 +2019,178 @@ make_browser_sidebar(FivIoModel *model)
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
make_menu_bar(void)
|
||||
// --- Actions -----------------------------------------------------------------
|
||||
|
||||
#define ACTION(name) static void on_action_ ## name(void)
|
||||
|
||||
ACTION(new_window) {
|
||||
if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
|
||||
spawn_uri(g.uri);
|
||||
else
|
||||
spawn_uri(g.directory);
|
||||
}
|
||||
|
||||
ACTION(quit) {
|
||||
gtk_widget_destroy(g.window);
|
||||
}
|
||||
|
||||
ACTION(location) {
|
||||
fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
|
||||
}
|
||||
|
||||
ACTION(preferences) {
|
||||
show_preferences(g.window);
|
||||
}
|
||||
|
||||
ACTION(about) {
|
||||
show_about_dialog(g.window);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
typedef struct {
|
||||
const char *name; ///< Unprefixed action name
|
||||
GCallback callback; ///< Simple callback
|
||||
const char **accels; ///< NULL-terminated accelerator list
|
||||
} ActionEntry;
|
||||
|
||||
static ActionEntry g_actions[] = {
|
||||
{"preferences", on_action_preferences,
|
||||
(const char *[]) {"<Primary>comma", NULL}},
|
||||
{"new-window", on_action_new_window,
|
||||
(const char *[]) {"<Primary>n", NULL}},
|
||||
{"open", on_open,
|
||||
(const char *[]) {"<Primary>o", "o", NULL}},
|
||||
{"quit", on_action_quit,
|
||||
(const char *[]) {"<Primary>q", "<Primary>w", "q", NULL}},
|
||||
{"toggle-fullscreen", toggle_fullscreen,
|
||||
(const char *[]) {"F11", "f", NULL}},
|
||||
{"toggle-sunlight", toggle_sunlight,
|
||||
(const char *[]) {"<Alt><Shift>d", NULL}},
|
||||
{"go-back", go_back,
|
||||
(const char *[]) {"<Alt>Left", "BackSpace", NULL}},
|
||||
{"go-forward", go_forward,
|
||||
(const char *[]) {"<Alt>Right", NULL}},
|
||||
{"go-location", on_action_location,
|
||||
(const char *[]) {"<Primary>l", NULL}},
|
||||
{"help", show_help_contents,
|
||||
(const char *[]) {"F1", NULL}},
|
||||
{"shortcuts", show_help_shortcuts,
|
||||
// Similar to win.show-help-overlay in gtkapplication.c.
|
||||
(const char *[]) {"<Primary>question", "<Primary>F1", NULL}},
|
||||
{"about", on_action_about,
|
||||
(const char *[]) {"<Shift>F1", NULL}},
|
||||
{}
|
||||
};
|
||||
|
||||
static void
|
||||
dispatch_action(G_GNUC_UNUSED GSimpleAction *action,
|
||||
G_GNUC_UNUSED GVariant *parameter, gpointer user_data)
|
||||
{
|
||||
g.menu = gtk_menu_bar_new();
|
||||
GCallback callback = user_data;
|
||||
callback();
|
||||
}
|
||||
|
||||
GtkWidget *item_quit = gtk_menu_item_new_with_mnemonic("_Quit");
|
||||
g_signal_connect_swapped(item_quit, "activate",
|
||||
G_CALLBACK(gtk_widget_destroy), g.window);
|
||||
static void
|
||||
set_up_action(GtkApplication *app, const ActionEntry *a)
|
||||
{
|
||||
GSimpleAction *action = g_simple_action_new(a->name, NULL);
|
||||
g_signal_connect(action, "activate",
|
||||
G_CALLBACK(dispatch_action), a->callback);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action));
|
||||
g_object_unref(action);
|
||||
|
||||
GtkWidget *menu_file = gtk_menu_new();
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit);
|
||||
GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File");
|
||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_file), menu_file);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_file);
|
||||
gchar *full_name = g_strdup_printf("app.%s", a->name);
|
||||
gtk_application_set_accels_for_action(app, full_name, a->accels);
|
||||
g_free(full_name);
|
||||
}
|
||||
|
||||
GtkWidget *item_contents = gtk_menu_item_new_with_mnemonic("_Contents");
|
||||
g_signal_connect_swapped(item_contents, "activate",
|
||||
G_CALLBACK(show_help_contents), NULL);
|
||||
GtkWidget *item_shortcuts =
|
||||
gtk_menu_item_new_with_mnemonic("_Keyboard Shortcuts");
|
||||
g_signal_connect_swapped(item_shortcuts, "activate",
|
||||
G_CALLBACK(show_help_shortcuts), NULL);
|
||||
GtkWidget *item_about = gtk_menu_item_new_with_mnemonic("_About");
|
||||
g_signal_connect_swapped(item_about, "activate",
|
||||
G_CALLBACK(show_about_dialog), g.window);
|
||||
// --- Menu --------------------------------------------------------------------
|
||||
|
||||
GtkWidget *menu_help = gtk_menu_new();
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about);
|
||||
GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help");
|
||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_help), menu_help);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_help);
|
||||
typedef struct {
|
||||
const char *label; ///< Label, with a mnemonic
|
||||
const char *action; ///< Prefixed action name
|
||||
gboolean macos; ///< Show in the macOS global menu?
|
||||
} MenuItem;
|
||||
|
||||
typedef struct {
|
||||
const char *label; ///< Label, with a mnemonic
|
||||
const MenuItem *items; ///< ""-sectioned menu items
|
||||
} MenuRoot;
|
||||
|
||||
// We're single-instance, skip the "win" namespace for simplicity.
|
||||
static MenuRoot g_menu[] = {
|
||||
{"_File", (MenuItem[]) {
|
||||
{"_New Window", "app.new-window", TRUE},
|
||||
{"_Open...", "app.open", TRUE},
|
||||
{"", NULL, TRUE},
|
||||
{"_Quit", "app.quit", FALSE},
|
||||
{}
|
||||
}},
|
||||
{"_Go", (MenuItem[]) {
|
||||
{"_Back", "app.go-back", TRUE},
|
||||
{"_Forward", "app.go-forward", TRUE},
|
||||
{"", NULL, TRUE},
|
||||
{"_Location...", "app.go-location", TRUE},
|
||||
{}
|
||||
}},
|
||||
{"_Help", (MenuItem[]) {
|
||||
{"_Contents", "app.help", TRUE},
|
||||
{"_Keyboard Shortcuts", "app.shortcuts", TRUE},
|
||||
{"_About", "app.about", FALSE},
|
||||
{}
|
||||
}},
|
||||
{}
|
||||
};
|
||||
|
||||
static GMenuModel *
|
||||
make_submenu(const MenuItem *items)
|
||||
{
|
||||
GMenu *menu = g_menu_new();
|
||||
while (items->label) {
|
||||
GMenu *section = g_menu_new();
|
||||
for (; items->label; items++) {
|
||||
// Empty strings are interpreted as separators.
|
||||
if (!*items->label) {
|
||||
items++;
|
||||
break;
|
||||
}
|
||||
|
||||
GMenuItem *subitem = g_menu_item_new(items->label, items->action);
|
||||
if (!items->macos) {
|
||||
g_menu_item_set_attribute(
|
||||
subitem, "hidden-when", "s", "macos-menubar");
|
||||
}
|
||||
|
||||
g_menu_append_item(section, subitem);
|
||||
g_object_unref(subitem);
|
||||
}
|
||||
g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
|
||||
g_object_unref(section);
|
||||
}
|
||||
return G_MENU_MODEL(menu);
|
||||
}
|
||||
|
||||
static GMenuModel *
|
||||
make_menu_model(void)
|
||||
{
|
||||
GMenu *menu = g_menu_new();
|
||||
for (const MenuRoot *root = g_menu; root->label; root++) {
|
||||
GMenuModel *submenu = make_submenu(root->items);
|
||||
g_menu_append_submenu(menu, root->label, submenu);
|
||||
g_object_unref(submenu);
|
||||
}
|
||||
return G_MENU_MODEL(menu);
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
make_menu_bar(GMenuModel *model)
|
||||
{
|
||||
g.menu = gtk_menu_bar_new_from_model(model);
|
||||
|
||||
// Don't let it take up space by default. Firefox sets a precedent here.
|
||||
// (gtk_application_window_set_show_menubar() doesn't seem viable for use
|
||||
// for this purpose.)
|
||||
gtk_widget_show_all(g.menu);
|
||||
gtk_widget_set_no_show_all(g.menu, TRUE);
|
||||
gtk_widget_hide(g.menu);
|
||||
@@ -2116,6 +2198,8 @@ make_menu_bar(void)
|
||||
return g.menu;
|
||||
}
|
||||
|
||||
// --- Application -------------------------------------------------------------
|
||||
|
||||
// This is incredibly broken https://stackoverflow.com/a/51054396/76313
|
||||
// thus resolving the problem using overlaps.
|
||||
// We're trying to be universal for light and dark themes both. It's hard.
|
||||
@@ -2124,7 +2208,10 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
|
||||
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
|
||||
fiv-view, fiv-browser { background: @content_view_bg; } \
|
||||
placessidebar.fiv box > separator { margin: 4px 0; } \
|
||||
placessidebar.fiv row { min-height: 2em; } \
|
||||
.fiv-toolbar button { padding-left: 0; padding-right: 0; } \
|
||||
.fiv-toolbar button.text-button { \
|
||||
padding-left: 4px; padding-right: 4px; } \
|
||||
.fiv-toolbar > button:first-child { padding-left: 4px; } \
|
||||
.fiv-toolbar > button:last-child { padding-right: 4px; } \
|
||||
.fiv-toolbar separator { \
|
||||
@@ -2300,10 +2387,27 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
|
||||
g_signal_connect(g.window, "window-state-event",
|
||||
G_CALLBACK(on_window_state_event), NULL);
|
||||
|
||||
for (const ActionEntry *a = g_actions; a->name; a++)
|
||||
set_up_action(GTK_APPLICATION(app), a);
|
||||
|
||||
// GtkApplicationWindow overrides GtkContainer/GtkWidget virtual methods
|
||||
// so that it has the menu bar as an extra child (if it so decides).
|
||||
// However, we currently want this menu bar to only show on a key press,
|
||||
// and to hide as soon as it's no longer being used.
|
||||
// Messing with the window's internal state seems at best quirky,
|
||||
// so we'll manage the menu entirely by ourselves.
|
||||
gtk_application_window_set_show_menubar(
|
||||
GTK_APPLICATION_WINDOW(g.window), FALSE);
|
||||
|
||||
GMenuModel *menu = make_menu_model();
|
||||
gtk_application_set_menubar(GTK_APPLICATION(app), menu);
|
||||
// The default "app menu" is good, in particular for macOS, so keep it.
|
||||
|
||||
GtkWidget *menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar());
|
||||
gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar(menu));
|
||||
gtk_container_add(GTK_CONTAINER(menu_box), g.stack);
|
||||
gtk_container_add(GTK_CONTAINER(g.window), menu_box);
|
||||
g_object_unref(menu);
|
||||
|
||||
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
|
||||
if (g_settings_get_boolean(settings, "dark-theme"))
|
||||
@@ -2348,7 +2452,7 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
|
||||
}
|
||||
|
||||
static struct {
|
||||
gboolean browse, extract_thumbnail;
|
||||
gboolean browse, collection, extract_thumbnail;
|
||||
gchar **args, *thumbnail_size, *thumbnail_size_search;
|
||||
} o;
|
||||
|
||||
@@ -2358,12 +2462,12 @@ on_app_activate(
|
||||
{
|
||||
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
|
||||
// interpret multiple command line arguments differently, as a collection.
|
||||
// However, single-element collections are unrepresentable this way.
|
||||
// Should we allow multiple targets only in a special new mode?
|
||||
// However, single-element collections are unrepresentable this way,
|
||||
// so we have a switch to enforce it.
|
||||
g.files_index = -1;
|
||||
if (o.args) {
|
||||
const gchar *target = *o.args;
|
||||
if (o.args[1]) {
|
||||
if (o.args[1] || o.collection) {
|
||||
fiv_collection_reload(o.args);
|
||||
target = FIV_COLLECTION_SCHEME ":/";
|
||||
}
|
||||
@@ -2479,11 +2583,6 @@ on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO(p): Use Little CMS with contexts instead.
|
||||
#ifdef HAVE_LCMS2_FAST_FLOAT
|
||||
cmsPlugin(cmsFastFloatExtensions());
|
||||
#endif // HAVE_LCMS2_FAST_FLOAT
|
||||
|
||||
// Normalize all arguments to URIs, and run thumbnailing modes first.
|
||||
for (gsize i = 0; o.args && o.args[i]; i++) {
|
||||
GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
|
||||
@@ -2514,6 +2613,9 @@ main(int argc, char *argv[])
|
||||
{"browse", 0, G_OPTION_FLAG_IN_MAIN,
|
||||
G_OPTION_ARG_NONE, &o.browse,
|
||||
"Start in filesystem browsing mode", NULL},
|
||||
{"collection", 0, G_OPTION_FLAG_IN_MAIN,
|
||||
G_OPTION_ARG_NONE, &o.collection,
|
||||
"Always put arguments in a collection (implies --browse)", NULL},
|
||||
{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
|
||||
G_OPTION_ARG_NONE, NULL,
|
||||
"Invalidate the wide thumbnail cache", NULL},
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
double buffering.
|
||||
</description>
|
||||
</key>
|
||||
<key name='opengl' type='b'>
|
||||
<default>false</default>
|
||||
<summary>Use experimental OpenGL rendering</summary>
|
||||
<description>
|
||||
OpenGL within GTK+ is highly problematic--you don't want this.
|
||||
</description>
|
||||
</key>
|
||||
<key name='dark-theme' type='b'>
|
||||
<default>false</default>
|
||||
<summary>Use a dark theme variant on start-up</summary>
|
||||
|
||||
71
fiv.wxs.in
Normal file
71
fiv.wxs.in
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||
<?define FullName = "@ProjectName@ @ProjectVersion@" ?>
|
||||
<?if $(sys.BUILDARCH) = x64 ?>
|
||||
<?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else?>
|
||||
<?define ProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?endif?>
|
||||
|
||||
<Product Id='*'
|
||||
Name='$(var.FullName)'
|
||||
UpgradeCode='a3e64e2d-4310-4c5f-8562-bb0e0b3e0a53'
|
||||
Language='1033'
|
||||
Codepage='1252'
|
||||
Version='@ProjectVersion@'
|
||||
Manufacturer='Premysl Eric Janouch'>
|
||||
|
||||
<Package Id='*'
|
||||
Keywords='Installer,Image,Viewer'
|
||||
Description='$(var.FullName) Installer'
|
||||
Manufacturer='Premysl Eric Janouch'
|
||||
InstallerVersion='200'
|
||||
Compressed='yes'
|
||||
Languages='1033'
|
||||
SummaryCodepage='1252' />
|
||||
|
||||
<Media Id='1' Cabinet='data.cab' EmbedCab='yes' />
|
||||
<Icon Id='fiv.ico' SourceFile='fiv.ico' />
|
||||
<Property Id='ARPPRODUCTICON' Value='fiv.ico' />
|
||||
<Property Id='ARPURLINFOABOUT' Value='@ProjectURL@' />
|
||||
|
||||
<UIRef Id='WixUI_Minimal' />
|
||||
<!-- This isn't supported by msitools, but is necessary for WiX.
|
||||
<WixVariable Id='WixUILicenseRtf' Value='License.rtf' />
|
||||
-->
|
||||
|
||||
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||
<Directory Id='$(var.ProgramFilesFolder)'>
|
||||
<Directory Id='INSTALLDIR' Name='$(var.FullName)' />
|
||||
</Directory>
|
||||
|
||||
<Directory Id='ProgramMenuFolder'>
|
||||
<Directory Id='ProgramMenuDir' Name='$(var.FullName)' />
|
||||
</Directory>
|
||||
|
||||
<Directory Id='DesktopFolder' />
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id='ProgramMenuDir'>
|
||||
<Component Id='ProgramMenuDir' Guid='*'>
|
||||
<Shortcut Id='ProgramsMenuShortcut'
|
||||
Name='@ProjectName@'
|
||||
Target='[INSTALLDIR]\fiv.exe'
|
||||
WorkingDirectory='INSTALLDIR'
|
||||
Arguments='"%USERPROFILE%"'
|
||||
Icon='fiv.ico' />
|
||||
<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
|
||||
<RegistryValue Root='HKCU'
|
||||
Key='Software\[Manufacturer]\[ProductName]'
|
||||
Type='string'
|
||||
Value=''
|
||||
KeyPath='yes' />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentGroupRef Id='CG.fiv' />
|
||||
<ComponentRef Id='ProgramMenuDir' />
|
||||
</Feature>
|
||||
</Product>
|
||||
</Wix>
|
||||
48
meson.build
48
meson.build
@@ -1,7 +1,7 @@
|
||||
# vim: noet ts=4 sts=4 sw=4:
|
||||
project('fiv', 'c',
|
||||
default_options : ['c_std=gnu99', 'warning_level=2'],
|
||||
version : '0.1.0',
|
||||
version : '1.0.0',
|
||||
meson_version : '>=0.57')
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
@@ -36,7 +36,9 @@ gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
|
||||
dependencies = [
|
||||
dependency('gtk+-3.0'),
|
||||
dependency('pixman-1'),
|
||||
dependency('epoxy'),
|
||||
|
||||
dependency('libjpeg'),
|
||||
dependency('libturbojpeg'),
|
||||
dependency('libwebp'),
|
||||
dependency('libwebpdemux'),
|
||||
@@ -93,11 +95,13 @@ endif
|
||||
# XXX: https://github.com/mesonbuild/meson/issues/825
|
||||
docdir = get_option('datadir') / 'doc' / meson.project_name()
|
||||
application_ns = 'name.janouch.'
|
||||
application_url = 'https://janouch.name/p/' + meson.project_name()
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set_quoted('PROJECT_NAME', meson.project_name())
|
||||
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
|
||||
conf.set_quoted('PROJECT_NS', application_ns)
|
||||
conf.set_quoted('PROJECT_URL', application_url)
|
||||
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
|
||||
if win32
|
||||
conf.set_quoted('PROJECT_DOCDIR', docdir)
|
||||
@@ -161,7 +165,8 @@ tiff_tables = custom_target('tiff-tables.h',
|
||||
)
|
||||
|
||||
desktops = ['fiv.desktop', 'fiv-browse.desktop']
|
||||
iolib = static_library('fiv-io', 'fiv-io.c', 'xdg.c', tiff_tables,
|
||||
iolib = static_library('fiv-io', 'fiv-io.c', 'fiv-io-cmm.c', 'xdg.c',
|
||||
tiff_tables, config,
|
||||
dependencies : dependencies).extract_all_objects(recursive : true)
|
||||
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
|
||||
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
|
||||
@@ -183,13 +188,13 @@ jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
|
||||
)
|
||||
|
||||
if get_option('tools').enabled()
|
||||
# libjq 1.6 lacks a pkg-config file, and there is no release in sight.
|
||||
# libjq 1.6 is required.
|
||||
# libjq has only received a pkg-config file in version 1.7.
|
||||
# libjq >= 1.6 is required.
|
||||
tools_dependencies = [
|
||||
cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
|
||||
tools_c_args = cc.get_supported_arguments(
|
||||
'-Wno-unused-function', '-Wno-unused-parameter')
|
||||
foreach tool : ['info', 'pnginfo', 'rawinfo']
|
||||
foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
|
||||
executable(tool, 'tools/' + tool + '.c', tiff_tables,
|
||||
dependencies : tools_dependencies,
|
||||
c_args: tools_c_args)
|
||||
@@ -215,6 +220,7 @@ endforeach
|
||||
|
||||
# For the purposes of development: make the program find its GSettings schemas.
|
||||
gnome.compile_schemas(depend_files : files(gsettings_schemas))
|
||||
gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
|
||||
|
||||
# Meson is broken on Windows and removes the backslashes, so this ends up empty.
|
||||
symbolics = run_command(find_program('sed', required : false, disabler : true),
|
||||
@@ -333,11 +339,43 @@ if not win32
|
||||
if not meson.is_cross_build()
|
||||
meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
|
||||
endif
|
||||
|
||||
# Quick and dirty package generation, lacking dependencies.
|
||||
packaging = configuration_data({
|
||||
'name' : meson.project_name(),
|
||||
'version' : meson.project_version(),
|
||||
'summary' : 'Image viewer',
|
||||
'author' : 'Přemysl Eric Janouch',
|
||||
})
|
||||
|
||||
subdir('submodules/liberty/meson/packaging')
|
||||
elif meson.is_cross_build()
|
||||
# Note that even compiling /from within MSYS2/ can still be a cross-build.
|
||||
msys2_root = meson.get_external_property('msys2_root')
|
||||
meson.add_install_script('msys2-install.sh', msys2_root)
|
||||
|
||||
wxs = configure_file(
|
||||
input : 'fiv.wxs.in',
|
||||
output : 'fiv.wxs',
|
||||
configuration : configuration_data({
|
||||
'ProjectName' : meson.project_name(),
|
||||
'ProjectVersion' : meson.project_version(),
|
||||
'ProjectURL' : application_url,
|
||||
}),
|
||||
)
|
||||
msi = meson.project_name() + '-' + meson.project_version() + \
|
||||
'-' + host_machine.cpu() + '.msi'
|
||||
custom_target('package',
|
||||
output : msi,
|
||||
command : [meson.current_source_dir() / 'msys2-package.sh',
|
||||
host_machine.cpu(), msi, wxs],
|
||||
env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
|
||||
'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
|
||||
console : true,
|
||||
build_always_stale : true,
|
||||
build_by_default : false,
|
||||
)
|
||||
|
||||
# This is the minimum to run targets from msys2-configure.sh builds.
|
||||
meson.add_devenv({
|
||||
'WINEPATH' : msys2_root / 'bin',
|
||||
|
||||
@@ -19,7 +19,7 @@ then
|
||||
wine64() { "$@"; }
|
||||
awk() { command awk -v RS='\r?\n' "$@"; }
|
||||
pacman -S --needed libarchive $pkg-ca-certificates $pkg-gcc $pkg-icoutils \
|
||||
$pkg-librsvg $pkg-meson $pkg-pkgconf
|
||||
$pkg-librsvg $pkg-meson $pkg-msitools $pkg-pkgconf
|
||||
fi
|
||||
|
||||
status() {
|
||||
@@ -46,7 +46,8 @@ fetch() {
|
||||
} BEGIN { while ((getline < "db.tsv") > 0) {
|
||||
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
|
||||
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
|
||||
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | while IFS= read -r name
|
||||
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
|
||||
while IFS= read -r name
|
||||
do
|
||||
status Fetching "$name"
|
||||
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
|
||||
@@ -69,15 +70,20 @@ extract() {
|
||||
for subdir in *
|
||||
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
|
||||
done
|
||||
for i in packages/*
|
||||
do bsdtar -xf "$i" --strip-components 1 \
|
||||
while IFS= read -r name
|
||||
do bsdtar -xf "packages/$name" --strip-components 1 \
|
||||
--exclude '*/share/man' --exclude '*/share/doc'
|
||||
done
|
||||
done < db.want
|
||||
|
||||
bsdtar -xf exiftool.tar.gz
|
||||
mv Image-ExifTool-*/exiftool bin
|
||||
mv Image-ExifTool-*/lib/* lib/perl5/site_perl
|
||||
rm -rf Image-ExifTool-*
|
||||
# Don't require Perl, which may not exist anymore on i686:
|
||||
# https://github.com/msys2/MINGW-packages/pull/20085
|
||||
if [ -d lib/perl5 ]
|
||||
then
|
||||
bsdtar -xf exiftool.tar.gz
|
||||
mv Image-ExifTool-*/exiftool bin
|
||||
mv Image-ExifTool-*/lib/* lib/perl5/site_perl
|
||||
rm -rf Image-ExifTool-*
|
||||
fi
|
||||
}
|
||||
|
||||
configure() {
|
||||
@@ -120,13 +126,12 @@ setup() {
|
||||
endian = 'little'
|
||||
EOF
|
||||
|
||||
meson setup --buildtype=debugoptimized --prefix="$packagedir" \
|
||||
meson setup --buildtype=debugoptimized --prefix=/ \
|
||||
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
|
||||
}
|
||||
|
||||
sourcedir=$(realpath "${2:-$(dirname "$0")}")
|
||||
builddir=$(realpath "${1:-builddir}")
|
||||
packagedir=$builddir/package
|
||||
toolchain=$builddir/msys2-cross-toolchain.meson
|
||||
|
||||
# This directory name matches the prefix in .pc files, so we don't need to
|
||||
@@ -136,7 +141,7 @@ msys2_root=$builddir$prefix
|
||||
mkdir -p "$msys2_root"
|
||||
cd "$msys2_root"
|
||||
dbsync
|
||||
fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-perl \
|
||||
fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-libjxl $pkg-perl \
|
||||
$pkg-perl-win32-api $pkg-libwinpthread-git # Because we don't do "provides"?
|
||||
verify
|
||||
extract
|
||||
|
||||
@@ -12,23 +12,24 @@ fi
|
||||
|
||||
# Copy binaries we directly or indirectly depend on.
|
||||
cp -p "$msys2_root"/bin/*.dll .
|
||||
cp -p "$msys2_root"/bin/wperl.exe .
|
||||
cp -p "$msys2_root"/bin/exiftool .
|
||||
cp -p "$msys2_root"/bin/wperl.exe . || :
|
||||
cp -p "$msys2_root"/bin/exiftool . || :
|
||||
# The console helper is only useful for debug builds.
|
||||
cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
|
||||
cp -pR "$msys2_root"/etc/ .
|
||||
|
||||
mkdir -p lib
|
||||
cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib
|
||||
cp -pR "$msys2_root"/lib/perl5/ lib
|
||||
cp -pR "$msys2_root"/lib/perl5/ lib || :
|
||||
mkdir -p share/glib-2.0/schemas
|
||||
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
|
||||
mkdir -p share
|
||||
cp -pR "$msys2_root"/share/mime/ share
|
||||
mkdir -p share/icons
|
||||
cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons
|
||||
mkdir -p share/icons/hicolor
|
||||
cp -p "$msys2_root"/share/icons/hicolor/index.theme share/icons/hicolor
|
||||
mkdir -p share/mime
|
||||
# GIO doesn't use the database on Windows, this subset is for us.
|
||||
find "$msys2_root"/share/mime/ -maxdepth 1 -type f -exec cp -p {} share/mime \;
|
||||
|
||||
# Remove unreferenced libraries.
|
||||
find lib -name '*.a' -exec rm -- {} +
|
||||
|
||||
35
msys2-package.sh
Executable file
35
msys2-package.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh -e
|
||||
export LC_ALL=C
|
||||
cd "$MESON_BUILD_ROOT"
|
||||
arch=$1 msi=$2 files=package-files.wxs
|
||||
destdir=$(pwd)/package/${msi%.*}
|
||||
shift 2
|
||||
|
||||
# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
|
||||
[ "$arch" = "x86" ] || arch=x64
|
||||
|
||||
rm -rf "$destdir"
|
||||
meson install --destdir "$destdir"
|
||||
|
||||
txt2rtf() {
|
||||
LC_ALL=C.UTF-8 iconv -f utf-8 -t ascii//translit "$@" | awk 'BEGIN {
|
||||
print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
|
||||
print "\\f0\\fs24{\\pard\\sa240"
|
||||
} {
|
||||
gsub(/\\/, "\\\\"); gsub(/[{]/, "\\{"); gsub(/[}]/, "\\}")
|
||||
if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
|
||||
else { print prefix $0; prefix = " " }
|
||||
} END {
|
||||
print "\\par}}"
|
||||
}'
|
||||
}
|
||||
|
||||
# msitools have this filename hardcoded in UI files, and it's required.
|
||||
txt2rtf "$MESON_SOURCE_ROOT/LICENSE" > License.rtf
|
||||
|
||||
find "$destdir" -type f \
|
||||
| wixl-heat --prefix "$destdir/" --directory-ref INSTALLDIR \
|
||||
--component-group CG.fiv --var var.SourceDir > "$files"
|
||||
|
||||
wixl --verbose --arch "$arch" -D SourceDir="$destdir" --ext ui \
|
||||
--output "$msi" "$@" "$files"
|
||||
@@ -1,8 +1,8 @@
|
||||
[wrap-file]
|
||||
directory = jpeg-quantsmooth-1.20210408
|
||||
source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20210408.tar.gz
|
||||
source_filename = jpeg-quantsmooth-1.20210408.tar.gz
|
||||
source_hash = 5937ca26db33888cab8638c1a8dc7a367a953bd0857ceb1290d5abc6febf3116
|
||||
directory = jpeg-quantsmooth-1.20230818
|
||||
source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20230818.tar.gz
|
||||
source_filename = jpeg-quantsmooth-1.20230818.tar.gz
|
||||
source_hash = ff9a62e8560851648c60d84b3d97ebd9769f01ce6b995779e071d19a759eca06
|
||||
patch_directory = libjpegqs
|
||||
|
||||
[provide]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# vim: noet ts=4 sts=4 sw=4:
|
||||
project('jpeg-qs', 'c')
|
||||
add_project_arguments(meson.get_compiler('c')
|
||||
.get_supported_arguments('-Wno-misleading-indentation'),
|
||||
'-DWITH_LOG', language : 'c')
|
||||
add_project_arguments('-DWITH_LOG', language : 'c')
|
||||
|
||||
deps = [
|
||||
dependency('libjpeg'),
|
||||
|
||||
210
tools/hotpixels.c
Normal file
210
tools/hotpixels.c
Normal file
@@ -0,0 +1,210 @@
|
||||
//
|
||||
// hotpixels.c: look for hot pixels in raw image files
|
||||
//
|
||||
// Usage: pass a bunch of raw photo images taken with the lens cap on at,
|
||||
// e.g., ISO 8000-12800 @ 1/20-1/60, and store the resulting file as,
|
||||
// e.g., Nikon D7500.badpixels, which can then be directly used by Rawtherapee.
|
||||
//
|
||||
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include <libraw.h>
|
||||
|
||||
#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
|
||||
#error LibRaw 0.21.0 or newer is required.
|
||||
#endif
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void *
|
||||
xreallocarray(void *o, size_t n, size_t m)
|
||||
{
|
||||
if (m && n > SIZE_MAX / m) {
|
||||
fprintf(stderr, "xreallocarray: %s\n", strerror(ENOMEM));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
void *p = realloc(o, n * m);
|
||||
if (!p && n && m) {
|
||||
fprintf(stderr, "xreallocarray: %s\n", strerror(errno));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct coord { ushort x, y; };
|
||||
|
||||
static bool
|
||||
coord_equals(struct coord a, struct coord b)
|
||||
{
|
||||
return a.x == b.x && a.y == b.y;
|
||||
}
|
||||
|
||||
static int
|
||||
coord_cmp(const void *a, const void *b)
|
||||
{
|
||||
const struct coord *ca = (const struct coord *) a;
|
||||
const struct coord *cb = (const struct coord *) b;
|
||||
return ca->y != cb->y
|
||||
? (int) ca->y - (int) cb->y
|
||||
: (int) ca->x - (int) cb->x;
|
||||
}
|
||||
|
||||
struct candidates {
|
||||
struct coord *xy;
|
||||
size_t len;
|
||||
size_t alloc;
|
||||
};
|
||||
|
||||
static void
|
||||
candidates_add(struct candidates *c, ushort x, ushort y)
|
||||
{
|
||||
if (c->len == c->alloc) {
|
||||
c->alloc += 64;
|
||||
c->xy = xreallocarray(c->xy, sizeof *c->xy, c->alloc);
|
||||
}
|
||||
|
||||
c->xy[c->len++] = (struct coord) {x, y};
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// A stretch of zeroes that is assumed to mean start of outliers.
|
||||
#define SPAN 10
|
||||
|
||||
static const char *
|
||||
process_raw(struct candidates *c, const uint8_t *p, size_t len)
|
||||
{
|
||||
libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
|
||||
if (!iprc)
|
||||
return "failed to obtain a LibRaw handle";
|
||||
|
||||
int err = 0;
|
||||
if ((err = libraw_open_buffer(iprc, p, len)) ||
|
||||
(err = libraw_unpack(iprc))) {
|
||||
libraw_close(iprc);
|
||||
return libraw_strerror(err);
|
||||
}
|
||||
if (!iprc->rawdata.raw_image) {
|
||||
libraw_close(iprc);
|
||||
return "only Bayer raws are supported, not Foveon";
|
||||
}
|
||||
|
||||
// Make a histogram.
|
||||
uint64_t bins[USHRT_MAX] = {};
|
||||
for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
|
||||
for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
|
||||
ushort y = iprc->sizes.top_margin + yy;
|
||||
ushort x = iprc->sizes.left_margin + xx;
|
||||
bins[iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x]]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Detecting outliers is not completely straight-forward,
|
||||
// it may help to see the histogram.
|
||||
if (getenv("HOTPIXELS_HISTOGRAM")) {
|
||||
for (ushort i = 0; i < USHRT_MAX; i++)
|
||||
fprintf(stderr, "%u ", (unsigned) bins[i]);
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
// Go to the first non-zero pixel value.
|
||||
size_t last = 0;
|
||||
for (; last < USHRT_MAX; last++)
|
||||
if (bins[last])
|
||||
break;
|
||||
|
||||
// Find the last pixel value we assume to not be hot.
|
||||
for (; last < USHRT_MAX - SPAN - 1; last++) {
|
||||
uint64_t nonzero = 0;
|
||||
for (int i = 1; i <= SPAN; i++)
|
||||
nonzero += bins[last + i];
|
||||
if (!nonzero)
|
||||
break;
|
||||
}
|
||||
|
||||
// Store coordinates for all pixels above that value.
|
||||
for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
|
||||
for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
|
||||
ushort y = iprc->sizes.top_margin + yy;
|
||||
ushort x = iprc->sizes.left_margin + xx;
|
||||
if (iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x] > last)
|
||||
candidates_add(c, xx, yy);
|
||||
}
|
||||
}
|
||||
|
||||
libraw_close(iprc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static const char *
|
||||
do_file(struct candidates *c, const char *filename)
|
||||
{
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
if (!fp)
|
||||
return strerror(errno);
|
||||
|
||||
uint8_t *data = NULL, buf[256 << 10];
|
||||
size_t n, len = 0;
|
||||
while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
|
||||
data = xreallocarray(data, len + n, 1);
|
||||
memcpy(data + len, buf, n);
|
||||
len += n;
|
||||
}
|
||||
|
||||
const char *err = ferror(fp)
|
||||
? strerror(errno)
|
||||
: process_raw(c, data, len);
|
||||
|
||||
fclose(fp);
|
||||
free(data);
|
||||
return err;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
struct candidates c = {};
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char *filename = argv[i], *err = do_file(&c, filename);
|
||||
if (err) {
|
||||
fprintf(stderr, "%s: %s\n", filename, err);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
qsort(c.xy, c.len, sizeof *c.xy, coord_cmp);
|
||||
|
||||
// If it is detected in all passed photos, it is probably indeed bad.
|
||||
int count = 1;
|
||||
for (size_t i = 1; i <= c.len; i++) {
|
||||
if (i != c.len && coord_equals(c.xy[i - 1], c.xy[i])) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count == argc - 1)
|
||||
printf("%u %u\n", c.xy[i - 1].x, c.xy[i - 1].y);
|
||||
|
||||
count = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
35
tools/info.h
35
tools/info.h
@@ -123,6 +123,39 @@ parse_exif_subifds(const struct tiffer *T, struct tiffer_entry *entry,
|
||||
return a;
|
||||
}
|
||||
|
||||
// Implemented partially, out of curiosity--it is not particularly useful,
|
||||
// because there is a ton more parsing to do here.
|
||||
static bool
|
||||
parse_exif_makernote(jv *v, const struct tiffer_entry *entry)
|
||||
{
|
||||
if (!getenv("INFO_MAKERNOTE") ||
|
||||
entry->tag != Exif_MakerNote || entry->type != TIFFER_UNDEFINED)
|
||||
return false;
|
||||
|
||||
struct tiffer T = {};
|
||||
if (entry->remaining_count >= 16 &&
|
||||
!memcmp(entry->p, "Nikon\x00\x02", 7) &&
|
||||
tiffer_init(&T, entry->p + 10, entry->remaining_count - 10) &&
|
||||
tiffer_next_ifd(&T)) {
|
||||
*v = parse_exif_ifd(&T, NULL);
|
||||
return true;
|
||||
}
|
||||
if (entry->remaining_count >= 16 &&
|
||||
!memcmp(entry->p, "Apple iOS\x00\x00\x01MM", 14)) {
|
||||
T.un = &tiffer_unbe;
|
||||
T.begin = T.p = entry->p + 14;
|
||||
T.end = entry->p + entry->remaining_count - 14;
|
||||
T.remaining_fields = 0;
|
||||
|
||||
struct tiffer subT = {};
|
||||
if (tiffer_subifd(&T, 0, &subT)) {
|
||||
*v = parse_exif_ifd(&subT, NULL);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static jv
|
||||
parse_exif_ascii(struct tiffer_entry *entry)
|
||||
{
|
||||
@@ -198,6 +231,8 @@ parse_exif_entry(jv o, const struct tiffer *T, struct tiffer_entry *entry,
|
||||
v = parse_exif_subifds(T, entry, subentries);
|
||||
} else if (entry->type == TIFFER_ASCII) {
|
||||
v = parse_exif_extract_sole_array_element(parse_exif_ascii(entry));
|
||||
} else if (info_begin == exif_entries && parse_exif_makernote(&v, entry)) {
|
||||
// Already processed.
|
||||
} else if (entry->type == TIFFER_UNDEFINED && !info->values) {
|
||||
// Several Exif entries of UNDEFINED type contain single-byte numbers.
|
||||
v = parse_exif_undefined(entry);
|
||||
|
||||
Reference in New Issue
Block a user