Compare commits

...

83 Commits

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

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

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

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

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

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

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

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

Even though C.UTF-8 may produce warnings, at least it works.
2023-06-27 00:36:00 +02:00
ce4a13ed38 msys2-install.sh: don't install the whole MIME DB 2023-06-27 00:36:00 +02:00
6a1b851130 Add libjxl to Windows packages
The library currently gets loaded through GdkPixbuf.
2023-06-26 21:38:59 +02:00
68245b55c9 msys2-configure: only extract what we need
In case the packages directory has been preloaded or symlinked.
2023-06-26 21:38:59 +02:00
2869c656c1 Centralize the project's URL 2023-06-26 15:46:10 +02:00
ec713b633e Package the MSI from within a custom target 2023-06-26 15:34:10 +02:00
88234f8283 Clean up the WiX XML a bit 2023-06-26 12:39:12 +02:00
49ee551b9b Use LocalAppData for thumbnails on Windows 2023-06-26 02:11:12 +02:00
089c90004b Produce a basic Windows installer package
We're very early adopters of msitools' new UI feature,
so this doesn't work on MSYS2 directly yet due to an old version.
2023-06-26 02:10:31 +02:00
19913a5e48 Only show X11-specific option when it makes sense 2023-06-25 03:39:24 +02:00
1ef0a84bc7 Fix build with older versions of Cairo 2023-06-25 02:12:50 +02:00
4b5b8ec9fa Implement our own Preferences dialog
And fix a resource leak.
2023-06-24 22:13:08 +02:00
3449ac5a12 Make GSettings find schema XMLs in devenv 2023-06-24 15:26:45 +02:00
bbfa2344d6 Fix colour management in animations
Bug introduced in d6e79cf.
2023-06-24 14:36:25 +02:00
2ff853b7e0 Improve looped animation behaviour 2023-06-24 14:36:24 +02:00
bb4d3acd12 Premultiply through Little CMS in animations 2023-06-24 14:36:24 +02:00
074bd4d37f Stop abusing Cairo user data, part 2
With the shift from cairo_surface_t, we've lost our ability
to directly render vector surfaces, but it doesn't matter.
2023-06-24 14:36:24 +02:00
add96b37a6 Stop abusing Cairo user data, part 1
This commit temporarily breaks multi-page images and animations.
2023-06-24 13:56:36 +02:00
c2e8b65d0f Don't rebuild fiv-io.c several times 2023-06-23 16:48:32 +02:00
4f57070e27 Fix 32-bit build warnings 2023-06-23 13:56:32 +02:00
2dc4e9c13b Make backspace go back in history
As on Windows.
2023-06-22 18:37:24 +02:00
a1f6ffd226 Make scripts capable of 32-bit Windows builds
Now binaries can be (cross-)built using GCC for 32- and 64-bit Windows.

Additional improvements:
 - Within MSYS2, try to install the required dependencies automatically.
 - Within MSYS2, fix passing libdir paths to pkg-config.
 - Prune documentation from extracted package files,
   addressing the incredible slowness of Windows filesystem operations.
 - Fix the script name in README.adoc instructions.
2023-06-22 18:33:31 +02:00
1eee1831a5 Windows seems to be mostly working fine 2023-06-22 11:05:04 +02:00
86622e0c31 Make cross-compilation scripts work from MSYS2
This is weird and runs very slowly.

Meson can also find libraries outside the subroot,
in particular the fast float plugin.
2023-06-22 04:06:38 +02:00
a4772ce319 Improve native MSYS2 build compatibility 2023-06-21 18:38:30 +02:00
0318424540 Handle LibTIFF errors correctly 2023-06-13 13:49:30 +02:00
8d5885bfdf Prevent a possibility of GdkPixbuf crashes 2023-06-13 13:36:24 +02:00
41b5ddc744 Fix thumbnailing with the GdkPixbuf loader 2023-06-13 13:21:03 +02:00
b308b5da18 Fix thumbnail extraction 2023-06-13 12:44:23 +02:00
1577961aa2 Improve compatibility with older dependencies 2023-06-10 11:52:49 +02:00
1fb42e689f Declare minimum Meson version
Due to our meson.add_install_script() usage, which results in a warning,
followed by an error.
2023-06-10 11:52:49 +02:00
8953e6beea Update comments 2023-06-09 13:13:17 +02:00
32 changed files with 2959 additions and 1479 deletions

View File

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

View File

@@ -2,7 +2,7 @@ fiv
=== ===
'fiv' is a slightly unconventional, general-purpose image browser and viewer 'fiv' is a slightly unconventional, general-purpose image browser and viewer
for Linux (as well as macOS and Windows, though these have known issues). for Linux and Windows (macOS still has major issues).
image::docs/fiv.webp["Screenshot of both the browser and the viewer"] image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
@@ -13,7 +13,7 @@ Features
photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
modules manage to load. modules manage to load.
- Employs high-performance file format libraries: Wuffs and libjpeg-turbo. - Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
- Makes use of 30-bit X.org visuals, whenever it's possible and appropriate. - Can make use of 30-bit X.org visuals, under certain conditions.
- Has a notion of pages, and tries to load all included content within files. - Has a notion of pages, and tries to load all included content within files.
- Can keep the zoom and position when browsing, to help with comparing - Can keep the zoom and position when browsing, to help with comparing
zoomed-in images. zoomed-in images.
@@ -33,15 +33,20 @@ Not necessarily in this order.
Packages Packages
-------- --------
Regular releases are sporadic. git master should be stable enough. You can get Regular releases are sporadic. git master should be stable enough.
a package with the latest development version from Archlinux's AUR. 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 Building and Running
-------------------- --------------------
Build-only dependencies: Build-only dependencies:
Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) + Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info, Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
libturbojpeg, libwebp, librsvg-2.0 (for icons) + libturbojpeg, libwebp, libepoxy, librsvg-2.0 (for icons) +
Optional dependencies: lcms2, Little CMS fast float plugin, Optional dependencies: lcms2, Little CMS fast float plugin,
LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool, LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
resvg (unstable API, needs to be requested explicitly) + resvg (unstable API, needs to be requested explicitly) +
@@ -49,30 +54,42 @@ Runtime dependencies for reverse image search:
xdg-utils, cURL, jq xdg-utils, cURL, jq
$ git clone --recursive https://git.janouch.name/p/fiv.git $ git clone --recursive https://git.janouch.name/p/fiv.git
$ cd fiv
$ meson setup builddir $ meson setup builddir
$ cd builddir $ cd builddir
$ meson compile $ 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 $ meson devenv fiv
The lossless JPEG cropper and reverse image search are intended to be invoked 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 Windows
~~~~~~~ ~~~~~~~
'fiv' can be cross-compiled for Windows, provided that you install a bunch of 'fiv' can be cross-compiled for Windows, provided that you install a bunch of
dependencies listed at the beginning of 'msys2-cross-configure.sh', 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. Beware that the build will take up about a gigabyte of disk space.
$ sh -e msys2-cross-configure.sh builddir $ 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 If everything succeeds, you will find a portable build of the application
in the 'builddir/package' subdirectory. Keep your expectations low. in the 'builddir/package' subdirectory, and a very basic MSI installation
package in 'builddir'.
Faster colour management
^^^^^^^^^^^^^^^^^^^^^^^^
To get the Little CMS fast float plugin, you'll have to enter MSYS2 and
https://www.msys2.org/wiki/Creating-Packages/#re-building-a-package[rebuild]
_mingw-w64-lcms2_ with the following change:
sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
Documentation Documentation
------------- -------------

View File

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

View File

@@ -16,7 +16,7 @@ q:lang(en):after { content: ""; }
<p class="details"> <p class="details">
<span id="author">Přemysl Eric Janouch</span><br> <span id="author">Přemysl Eric Janouch</span><br>
<span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br> <span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br>
<span id="revnumber">version 0.0.0,</span> <span id="revnumber">version 1.0.0,</span>
<span id="revdate">2023-04-17</span> <span id="revdate">2023-04-17</span>
<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes"> <p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
@@ -95,14 +95,8 @@ rm -rf ~/.cache/thumbnails/wide-*
<h2>Configuration</h2> <h2>Configuration</h2>
<p>The few configuration options <i>fiv</i> has can be adjusted using <p>To adjust the few configuration options of <i>fiv</i>,
<i>dconf-editor</i>, which can be launched in the appropriate location from press <kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>,</kbd> to open <i>Preferences</i>.
within the application by pressing <kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>,</kbd>.
For command line usage, there is the <i>gsettings</i> utility:
<pre>
gsettings list-recursively name.janouch.fiv
</pre>
<p>To make your changes take effect, restart <i>fiv</i>. <p>To make your changes take effect, restart <i>fiv</i>.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

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

View File

@@ -528,12 +528,16 @@ fiv_collection_file_query_info(GFile *file, const char *attributes,
g_file_info_set_name(info, basename); g_file_info_set_name(info, basename);
g_free(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); gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_display_name(info, prefixed); g_file_info_set_display_name(info, prefixed);
g_free(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); gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_edit_name(info, prefixed); g_file_info_set_edit_name(info, prefixed);
g_free(prefixed); g_free(prefixed);

View File

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

View File

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

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

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

View File

@@ -166,6 +166,10 @@ model_entry_array_new(void)
(GDestroyNotify) fiv_io_model_entry_unref); (GDestroyNotify) fiv_io_model_entry_unref);
} }
#if !GLIB_CHECK_VERSION(2, 70, 0)
#define g_pattern_spec_match g_pattern_match
#endif
static gboolean static gboolean
model_supports(FivIoModel *self, const char *filename) model_supports(FivIoModel *self, const char *filename)
{ {
@@ -243,7 +247,9 @@ static GPtrArray *
model_decide_placement( model_decide_placement(
FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files) FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
{ {
if (self->filtering && g_file_info_get_is_hidden(info)) if (self->filtering &&
g_file_info_has_attribute(info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
g_file_info_get_is_hidden(info))
return NULL; return NULL;
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
return subdirs; return subdirs;
@@ -342,6 +348,8 @@ static void
monitor_apply(enum monitor_event event, GPtrArray *target, int index, monitor_apply(enum monitor_event event, GPtrArray *target, int index,
FivIoModelEntry *new_entry) FivIoModelEntry *new_entry)
{ {
g_return_if_fail(event != MONITOR_CHANGING || index >= 0);
if (event == MONITOR_RENAMING && index < 0) if (event == MONITOR_RENAMING && index < 0)
// The file used to be filtered out but isn't anymore. // The file used to be filtered out but isn't anymore.
event = MONITOR_ADDING; event = MONITOR_ADDING;

1521
fiv-io.c

File diff suppressed because it is too large Load Diff

242
fiv-io.h
View File

@@ -1,7 +1,7 @@
// //
// fiv-io.h: image operations // fiv-io.h: image operations
// //
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name> // Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
// //
// Permission to use, copy, modify, and/or distribute this software for any // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // purpose with or without fee is hereby granted.
@@ -22,16 +22,53 @@
#include <glib.h> #include <glib.h>
#include <webp/encode.h> // WebPConfig #include <webp/encode.h> // WebPConfig
typedef enum _FivIoOrientation FivIoOrientation;
typedef struct _FivIoRenderClosure FivIoRenderClosure;
typedef struct _FivIoImage FivIoImage;
typedef struct _FivIoProfile FivIoProfile;
// --- Colour management ------------------------------------------------------- // --- Colour management -------------------------------------------------------
// Note that without a CMM, all FivIoCmm and FivIoProfile will be returned NULL.
// TODO(p): Make it also possible to use Skia's skcms. GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
typedef void *FivIoProfile; void fiv_io_profile_free(FivIoProfile *self);
FivIoProfile fiv_io_profile_new(const void *data, size_t len);
FivIoProfile fiv_io_profile_new_sRGB(void);
void fiv_io_profile_free(FivIoProfile self);
// From libwebp, verified to exactly match [x * a / 255]. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
#define FIV_TYPE_IO_CMM (fiv_io_cmm_get_type())
G_DECLARE_FINAL_TYPE(FivIoCmm, fiv_io_cmm, FIV, IO_CMM, GObject)
FivIoCmm *fiv_io_cmm_get_default(void);
FivIoProfile *fiv_io_cmm_get_profile(
FivIoCmm *self, const void *data, size_t len);
FivIoProfile *fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes);
FivIoProfile *fiv_io_cmm_get_profile_sRGB(FivIoCmm *self);
FivIoProfile *fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma);
FivIoProfile *fiv_io_cmm_get_profile_parametric(
FivIoCmm *self, double gamma, double whitepoint[2], double primaries[6]);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void fiv_io_premultiply_argb32(FivIoImage *image);
void fiv_io_cmm_cmyk(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
void fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
int w, int h, FivIoProfile *source, FivIoProfile *target);
void fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
#define fiv_io_cmm_argb32_premultiply_page(cmm, page, target) \
fiv_io_cmm_page((cmm), (page), (target), fiv_io_cmm_argb32_premultiply)
void fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
void (*frame_cb) (FivIoCmm *,
FivIoImage *, FivIoProfile *, FivIoProfile *));
void fiv_io_cmm_any(FivIoCmm *self,
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
FivIoImage *fiv_io_cmm_finish(FivIoCmm *self,
FivIoImage *image, FivIoProfile *target);
// --- Loading ----------------------------------------------------------------- // --- Loading -----------------------------------------------------------------
@@ -39,64 +76,126 @@ extern const char *fiv_io_supported_media_types[];
gchar **fiv_io_all_supported_media_types(void); gchar **fiv_io_all_supported_media_types(void);
// Userdata are typically attached to all Cairo surfaces in an animation. // https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
enum _FivIoOrientation {
FivIoOrientationUnknown = 0,
FivIoOrientation0 = 1,
FivIoOrientationMirror0 = 2,
FivIoOrientation180 = 3,
FivIoOrientationMirror180 = 4,
FivIoOrientationMirror270 = 5,
FivIoOrientation90 = 6,
FivIoOrientationMirror90 = 7,
FivIoOrientation270 = 8
};
/// GBytes with plain Exif/TIFF data. // TODO(p): Maybe make FivIoProfile a referencable type,
extern cairo_user_data_key_t fiv_io_key_exif; // then loaders could store it in their closures.
/// FivIoOrientation, as a uintptr_t. struct _FivIoRenderClosure {
extern cairo_user_data_key_t fiv_io_key_orientation;
/// GBytes with plain ICC profile data.
extern cairo_user_data_key_t fiv_io_key_icc;
/// GBytes with plain XMP data.
extern cairo_user_data_key_t fiv_io_key_xmp;
/// GBytes with a WebP's THUM chunk, used for our thumbnails.
extern cairo_user_data_key_t fiv_io_key_thum;
/// GHashTable with key-value pairs from PNG's tEXt, zTXt, iTXt chunks.
/// Currently only read by fiv_io_open_png_thumbnail().
extern cairo_user_data_key_t fiv_io_key_text;
/// The next frame in a sequence, as a surface, in a chain, pre-composited.
/// There is no wrap-around.
extern cairo_user_data_key_t fiv_io_key_frame_next;
/// The previous frame in a sequence, as a surface, in a chain, pre-composited.
/// This is a weak pointer that wraps around, and needn't be present
/// for static images.
extern cairo_user_data_key_t fiv_io_key_frame_previous;
/// Frame duration in milliseconds as an intptr_t.
extern cairo_user_data_key_t fiv_io_key_frame_duration;
/// How many times to repeat the animation, or zero for +inf, as a uintptr_t.
extern cairo_user_data_key_t fiv_io_key_loops;
/// The first frame of the next page, as a surface, in a chain.
/// There is no wrap-around.
extern cairo_user_data_key_t fiv_io_key_page_next;
/// The first frame of the previous page, as a surface, in a chain.
/// There is no wrap-around. This is a weak pointer.
extern cairo_user_data_key_t fiv_io_key_page_previous;
typedef struct _FivIoRenderClosure {
/// The rendering is allowed to fail, returning NULL. /// The rendering is allowed to fail, returning NULL.
cairo_surface_t *(*render)(struct _FivIoRenderClosure *, double scale); FivIoImage *(*render)(
} FivIoRenderClosure; FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
void (*destroy)(FivIoRenderClosure *);
};
/// A FivIoRenderClosure for parametrized re-rendering of vector formats. // Metadata are typically attached to all Cairo surfaces in an animation.
/// This is attached at the page level.
/// The rendered image will not have this key. struct _FivIoImage {
extern cairo_user_data_key_t fiv_io_key_render; uint8_t *data; ///< Raw image data
cairo_format_t format; ///< Data format
uint32_t width; ///< Width of the image in pixels
uint32_t stride; ///< Row stride in bytes
uint32_t height; ///< Height of the image in pixels
FivIoOrientation orientation; ///< Orientation to use for display
GBytes *exif; ///< Raw Exif/TIFF segment
GBytes *icc; ///< Raw ICC profile data
GBytes *xmp; ///< Raw XMP data
GBytes *thum; ///< WebP THUM chunk, for our thumbnails
/// GHashTable with key-value pairs from PNG's tEXt, zTXt, iTXt chunks.
/// Currently only read by fiv_io_open_png_thumbnail().
GHashTable *text;
/// A FivIoRenderClosure for parametrized re-rendering of vector formats.
/// This is attached at the page level.
FivIoRenderClosure *render;
/// The first frame of the next page, in a chain.
/// There is no wrap-around.
FivIoImage *page_next;
/// The first frame of the previous page, in a chain.
/// There is no wrap-around. This is a weak pointer.
FivIoImage *page_previous;
/// The next frame in a sequence, in a chain, pre-composited.
/// There is no wrap-around.
FivIoImage *frame_next;
/// The previous frame in a sequence, in a chain, pre-composited.
/// This is a weak pointer that wraps around,
/// and needn't be present for static images.
FivIoImage *frame_previous;
/// Frame duration in milliseconds.
int64_t frame_duration;
/// How many times to repeat the animation, or zero for +inf.
uint64_t loops;
};
FivIoImage *fiv_io_image_ref(FivIoImage *image);
void fiv_io_image_unref(FivIoImage *image);
/// Analogous to cairo_image_surface_create(). May return NULL.
FivIoImage *fiv_io_image_new(
cairo_format_t format, uint32_t width, uint32_t height);
/// Return a new Cairo image surface referencing the same data as the image,
/// eating the reference to it.
cairo_surface_t *fiv_io_image_to_surface(FivIoImage *image);
/// Return a new Cairo image surface referencing the same data as the image,
/// without eating the image's reference.
cairo_surface_t *fiv_io_image_to_surface_noref(const FivIoImage *image);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct { typedef struct {
const char *uri; ///< Source URI const char *uri; ///< Source URI
FivIoProfile screen_profile; ///< Target colour space or NULL FivIoCmm *cmm; ///< Colour management module or NULL
FivIoProfile *screen_profile; ///< Target colour space or NULL
int screen_dpi; ///< Target DPI int screen_dpi; ///< Target DPI
gboolean enhance; ///< Enhance JPEG (currently) gboolean enhance; ///< Enhance JPEG (currently)
gboolean first_frame_only; ///< Only interested in the 1st frame gboolean first_frame_only; ///< Only interested in the 1st frame
GPtrArray *warnings; ///< String vector for non-fatal errors GPtrArray *warnings; ///< String vector for non-fatal errors
} FivIoOpenContext; } FivIoOpenContext;
cairo_surface_t *fiv_io_open(const FivIoOpenContext *ctx, GError **error); FivIoImage *fiv_io_open(const FivIoOpenContext *ctx, GError **error);
cairo_surface_t *fiv_io_open_from_data( FivIoImage *fiv_io_open_from_data(
const char *data, size_t len, const FivIoOpenContext *ctx, GError **error); const char *data, size_t len, const FivIoOpenContext *ctx, GError **error);
cairo_surface_t *fiv_io_open_png_thumbnail(const char *path, GError **error);
FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error);
// --- Metadata ----------------------------------------------------------------
/// Returns a rendering matrix for an image (user space to pattern space),
/// 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);
/// Extracts the orientation field from Exif, if there's any.
FivIoOrientation fiv_io_exif_orientation(const guint8 *exif, gsize len);
/// Save metadata attached by this module in Exiv2 format.
gboolean fiv_io_save_metadata(
const FivIoImage *page, const char *path, GError **error);
// --- Thumbnail passing utilities --------------------------------------------- // --- Thumbnail passing utilities ---------------------------------------------
@@ -109,41 +208,12 @@ GBytes *fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error);
// --- Export ------------------------------------------------------------------ // --- Export ------------------------------------------------------------------
/// Encodes a Cairo surface as a WebP bitstream, following the configuration. /// Encodes an image as a WebP bitstream, following the configuration.
/// The result needs to be freed using WebPFree/WebPDataClear(). /// The result needs to be freed using WebPFree/WebPDataClear().
unsigned char *fiv_io_encode_webp( unsigned char *fiv_io_encode_webp(
cairo_surface_t *surface, const WebPConfig *config, size_t *len); FivIoImage *image, const WebPConfig *config, size_t *len);
/// Saves the page as a lossless WebP still picture or animation. /// Saves the page as a lossless WebP still picture or animation.
/// If no exact frame is specified, this potentially creates an animation. /// If no exact frame is specified, this potentially creates an animation.
gboolean fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
FivIoProfile target, const char *path, GError **error); FivIoProfile *target, const char *path, GError **error);
// --- Metadata ----------------------------------------------------------------
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
typedef enum _FivIoOrientation {
FivIoOrientationUnknown = 0,
FivIoOrientation0 = 1,
FivIoOrientationMirror0 = 2,
FivIoOrientation180 = 3,
FivIoOrientationMirror180 = 4,
FivIoOrientationMirror270 = 5,
FivIoOrientation90 = 6,
FivIoOrientationMirror90 = 7,
FivIoOrientation270 = 8
} FivIoOrientation;
/// Returns a rendering matrix for a surface (user space to pattern space),
/// and its target dimensions.
cairo_matrix_t fiv_io_orientation_apply(cairo_surface_t *surface,
FivIoOrientation orientation, double *width, double *height);
void fiv_io_orientation_dimensions(cairo_surface_t *surface,
FivIoOrientation orientation, double *width, double *height);
/// Extracts the orientation field from Exif, if there's any.
FivIoOrientation fiv_io_exif_orientation(const guint8 *exif, gsize len);
/// Save metadata attached by this module in Exiv2 format.
gboolean fiv_io_save_metadata(
cairo_surface_t *page, const char *path, GError **error);

View File

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

View File

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

View File

@@ -433,7 +433,10 @@ complete_path(GFile *location, GtkListStore *model)
!info) !info)
break; break;
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY || if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY)
continue;
if (g_file_info_has_attribute(info,
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
g_file_info_get_is_hidden(info)) g_file_info_get_is_hidden(info))
continue; continue;

View File

@@ -100,7 +100,16 @@ mark_thumbnail_lq(cairo_surface_t *surface)
static gchar * static gchar *
fiv_thumbnail_get_root(void) 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"); gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
#endif
gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL); gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL);
g_free(cache_dir); g_free(cache_dir);
return thumbnails_dir; return thumbnails_dir;
@@ -125,35 +134,37 @@ might_be_a_thumbnail(const char *path_or_uri)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static cairo_surface_t * static FivIoImage *
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error) render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
{ {
FivIoCmm *cmm = fiv_io_cmm_get_default();
FivIoOpenContext ctx = { FivIoOpenContext ctx = {
.uri = g_file_get_uri(target), .uri = g_file_get_uri(target),
.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, .screen_dpi = 96,
.first_frame_only = TRUE, .first_frame_only = TRUE,
// Only using this array as a redirect. // Only using this array as a redirect.
.warnings = g_ptr_array_new_with_free_func(g_free), .warnings = g_ptr_array_new_with_free_func(g_free),
}; };
cairo_surface_t *surface = fiv_io_open_from_data( FivIoImage *image = fiv_io_open_from_data(
g_bytes_get_data(data, NULL), g_bytes_get_size(data), &ctx, error); g_bytes_get_data(data, NULL), g_bytes_get_size(data), &ctx, error);
g_free((gchar *) ctx.uri); g_free((gchar *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE); g_ptr_array_free(ctx.warnings, TRUE);
if ((*color_managed = !!ctx.screen_profile)) if ((*color_managed = !!ctx.screen_profile))
fiv_io_profile_free(ctx.screen_profile); fiv_io_profile_free(ctx.screen_profile);
g_bytes_unref(data); g_bytes_unref(data);
return surface; return image;
} }
// In principle similar to rescale_thumbnail() from fiv-browser.c. // In principle similar to rescale_thumbnail() from fiv-browser.c.
static cairo_surface_t * static FivIoImage *
adjust_thumbnail(cairo_surface_t *thumbnail, double row_height) adjust_thumbnail(FivIoImage *thumbnail, double row_height)
{ {
// Hardcode orientation. // Hardcode orientation.
FivIoOrientation orientation = (uintptr_t) cairo_surface_get_user_data( FivIoOrientation orientation = thumbnail->orientation;
thumbnail, &fiv_io_key_orientation);
double w = 0, h = 0; double w = 0, h = 0;
cairo_matrix_t matrix = cairo_matrix_t matrix =
@@ -170,33 +181,46 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
} }
// Vector images should not have orientation, this should handle them all. // Vector images should not have orientation, this should handle them all.
FivIoRenderClosure *closure = FivIoRenderClosure *closure = thumbnail->render;
cairo_surface_get_user_data(thumbnail, &fiv_io_key_render);
if (closure && orientation <= FivIoOrientation0) { 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. // This API doesn't accept non-uniform scaling; prefer a vertical fit.
cairo_surface_t *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) if (scaled)
return scaled; return scaled;
} }
// This will be CAIRO_FORMAT_INVALID with non-image surfaces, which is fine. if (orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
cairo_format_t format = cairo_image_surface_get_format(thumbnail); return fiv_io_image_ref(thumbnail);
if (format != CAIRO_FORMAT_INVALID &&
orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
return cairo_surface_reference(thumbnail);
cairo_format_t format = thumbnail->format;
int projected_width = round(scale_x * w); int projected_width = round(scale_x * w);
int projected_height = round(scale_y * h); int projected_height = round(scale_y * h);
cairo_surface_t *scaled = cairo_image_surface_create( FivIoImage *scaled = fiv_io_image_new(
(format == CAIRO_FORMAT_RGB24 || format == CAIRO_FORMAT_RGB30) (format == CAIRO_FORMAT_RGB24 || format == CAIRO_FORMAT_RGB30)
? CAIRO_FORMAT_RGB24 ? CAIRO_FORMAT_RGB24
: CAIRO_FORMAT_ARGB32, : CAIRO_FORMAT_ARGB32,
projected_width, projected_height); projected_width, projected_height);
if (!scaled) {
g_warning("image allocation failure");
return fiv_io_image_ref(thumbnail);
}
cairo_surface_t *surface = fiv_io_image_to_surface_noref(scaled);
cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
cairo_t *cr = cairo_create(scaled);
cairo_scale(cr, scale_x, scale_y); cairo_scale(cr, scale_x, scale_y);
cairo_set_source_surface(cr, thumbnail, 0, 0); surface = fiv_io_image_to_surface_noref(thumbnail);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
cairo_pattern_t *pattern = cairo_get_source(cr); cairo_pattern_t *pattern = cairo_get_source(cr);
// CAIRO_FILTER_BEST, for some reason, works bad with CAIRO_FORMAT_RGB30. // CAIRO_FILTER_BEST, for some reason, works bad with CAIRO_FORMAT_RGB30.
cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD); cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD);
@@ -208,9 +232,7 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
// Note that this doesn't get triggered with oversize input surfaces, // Note that this doesn't get triggered with oversize input surfaces,
// even though nothing will be rendered. // even though nothing will be rendered.
if (cairo_surface_status(thumbnail) != CAIRO_STATUS_SUCCESS || if (cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
cairo_surface_status(scaled) != CAIRO_STATUS_SUCCESS ||
cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
cairo_status(cr) != CAIRO_STATUS_SUCCESS) cairo_status(cr) != CAIRO_STATUS_SUCCESS)
g_warning("thumbnail scaling failed"); g_warning("thumbnail scaling failed");
@@ -218,27 +240,32 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
return scaled; return scaled;
} }
static cairo_surface_t * static FivIoImage *
orient_thumbnail(cairo_surface_t *surface) orient_thumbnail(FivIoImage *image)
{ {
int orientation = (intptr_t) cairo_surface_get_user_data( if (image->orientation <= FivIoOrientation0)
surface, &fiv_io_key_orientation); return image;
if (orientation <= FivIoOrientation0)
return surface;
double w = 0, h = 0; double w = 0, h = 0;
cairo_matrix_t matrix = cairo_matrix_t matrix =
fiv_io_orientation_apply(surface, orientation, &w, &h); fiv_io_orientation_apply(image, image->orientation, &w, &h);
cairo_surface_t *oriented = FivIoImage *oriented = fiv_io_image_new(image->format, w, h);
cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h); if (!oriented) {
g_warning("image allocation failure");
return image;
}
cairo_t *cr = cairo_create(oriented); cairo_surface_t *surface = fiv_io_image_to_surface_noref(oriented);
cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
surface = fiv_io_image_to_surface(image);
cairo_set_source_surface(cr, surface, 0, 0); cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr); cairo_paint(cr);
cairo_destroy(cr); cairo_destroy(cr);
cairo_surface_destroy(surface);
return oriented; return oriented;
} }
@@ -286,7 +313,8 @@ extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
// because decoding a thumbnail will always be /much/ quicker than a render. // because decoding a thumbnail will always be /much/ quicker than a render.
// TODO(p): Maybe don't mark raw image thumbnails as low-quality // TODO(p): Maybe don't mark raw image thumbnails as low-quality
// if they're the right aspect ratio, and of sufficiently large size. // if they're the right aspect ratio, and of sufficiently large size.
// And I still worry about tflip. // The only downsides to camera-provided thumbnails seem to be cropping,
// and when they're decoded incorrectly. Also don't trust tflip.
float output_pixels = (float) iprc->sizes.iwidth * iprc->sizes.iheight; float output_pixels = (float) iprc->sizes.iwidth * iprc->sizes.iheight;
// Note that the ratio may even be larger than 1, as seen with CR2 files. // Note that the ratio may even be larger than 1, as seen with CR2 files.
while (i < count && while (i < count &&
@@ -331,11 +359,12 @@ extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
sorted[i]->tformat == LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB) sorted[i]->tformat == LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB)
i++; i++;
if (i < count) bool found = i != count;
if (found)
i = sorted[i] - iprc->thumbs_list.thumblist; i = sorted[i] - iprc->thumbs_list.thumblist;
g_free(sorted); g_free(sorted);
if (i == count) { if (!found) {
set_error(error, "no suitable thumbnails found"); set_error(error, "no suitable thumbnails found");
return FALSE; return FALSE;
} }
@@ -363,8 +392,7 @@ extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
// The main image's "flip" often matches up, but sometimes doesn't, e.g.: // The main image's "flip" often matches up, but sometimes doesn't, e.g.:
// - Phase One/H 25/H25_Outdoor_.IIQ // - Phase One/H 25/H25_Outdoor_.IIQ
// - Phase One/H 25/H25_IT8.7-2_Card.TIF // - Phase One/H 25/H25_IT8.7-2_Card.TIF
// - Leaf/Aptus 22/L_003172.mos (JPEG) *flip = iprc->sizes.flip;
*flip = iprc->sizes.flip
return TRUE; return TRUE;
} }
@@ -389,7 +417,7 @@ extract_libraw_unflip(int flip)
} }
} }
static cairo_surface_t * static FivIoImage *
extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error) extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
{ {
// Anything else is extremely rare. // Anything else is extremely rare.
@@ -398,24 +426,26 @@ extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
return NULL; return NULL;
} }
cairo_surface_t *surface = cairo_image_surface_create( FivIoImage *I = fiv_io_image_new(
CAIRO_FORMAT_RGB24, image->width, image->height); CAIRO_FORMAT_RGB24, image->width, image->height);
guint32 *out = (guint32 *) cairo_image_surface_get_data(surface); if (!I) {
const unsigned char *in = image->data; set_error(error, "image allocation failure");
for (guint64 i = 0; i < image->width * image->height; in += 3) return NULL;
out[i++] = in[0] << 16 | in[1] << 8 | in[2]; }
cairo_surface_mark_dirty(surface);
FivIoOrientation orient = extract_libraw_unflip(flip); guint32 *out = (guint32 *) I->data;
cairo_surface_set_user_data( const unsigned char *in = image->data;
surface, &fiv_io_key_orientation, (void *) (intptr_t) orient, NULL); for (guint64 i = 0; i < (guint64) image->width * image->height; in += 3)
return surface; out[i++] = in[0] << 16 | in[1] << 8 | in[2];
I->orientation = extract_libraw_unflip(flip);
return I;
} }
static cairo_surface_t * static FivIoImage *
extract_libraw(GFile *target, GMappedFile *mf, GError **error) extract_libraw(GFile *target, GMappedFile *mf, GError **error)
{ {
cairo_surface_t *surface = NULL; FivIoImage *I = NULL;
libraw_data_t *iprc = libraw_init( libraw_data_t *iprc = libraw_init(
LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK); LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
if (!iprc) { if (!iprc) {
@@ -457,7 +487,8 @@ extract_libraw(GFile *target, GMappedFile *mf, GError **error)
// - Samsung/NX200/2013-05-08-194524__sam6589.srw // - Samsung/NX200/2013-05-08-194524__sam6589.srw
// - Sony/DSC-HX95/DSC00018.ARW // - Sony/DSC-HX95/DSC00018.ARW
// Note that LibRaw inserts its own Exif segment if it doesn't find one, // Note that LibRaw inserts its own Exif segment if it doesn't find one,
// and this may differ from flip. // and this may differ from flip. It may also be wrong, as in:
// - Leaf/Aptus 22/L_003172.mos
// //
// Some files are problematic and we won't bother with special-casing: // Some files are problematic and we won't bother with special-casing:
// - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color. // - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color.
@@ -465,11 +496,11 @@ extract_libraw(GFile *target, GMappedFile *mf, GError **error)
switch (image->type) { switch (image->type) {
gboolean dummy; gboolean dummy;
case LIBRAW_IMAGE_JPEG: case LIBRAW_IMAGE_JPEG:
surface = render( I = render(
target, g_bytes_new(image->data, image->data_size), &dummy, error); target, g_bytes_new(image->data, image->data_size), &dummy, error);
break; break;
case LIBRAW_IMAGE_BITMAP: case LIBRAW_IMAGE_BITMAP:
surface = extract_libraw_bitmap(image, flip, error); I = extract_libraw_bitmap(image, flip, error);
break; break;
default: default:
set_error(error, "unsupported embedded thumbnail"); set_error(error, "unsupported embedded thumbnail");
@@ -478,7 +509,7 @@ extract_libraw(GFile *target, GMappedFile *mf, GError **error)
libraw_dcraw_clear_mem(image); libraw_dcraw_clear_mem(image);
fail: fail:
libraw_close(iprc); libraw_close(iprc);
return surface; return I;
} }
#endif // HAVE_LIBRAW #endif // HAVE_LIBRAW
@@ -504,30 +535,30 @@ fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
return NULL; return NULL;
} }
cairo_surface_t *surface = NULL; FivIoImage *image = NULL;
#ifdef HAVE_LIBRAW #ifdef HAVE_LIBRAW
surface = extract_libraw(target, mf, error); image = extract_libraw(target, mf, error);
#else // ! HAVE_LIBRAW #else // ! HAVE_LIBRAW
// TODO(p): Implement our own thumbnail extractors. // TODO(p): Implement our own thumbnail extractors.
set_error(error, "unsupported file"); set_error(error, "unsupported file");
#endif // ! HAVE_LIBRAW #endif // ! HAVE_LIBRAW
g_mapped_file_unref(mf); g_mapped_file_unref(mf);
if (!surface) if (!image)
return NULL; return NULL;
if (max_size < FIV_THUMBNAIL_SIZE_MIN || max_size > FIV_THUMBNAIL_SIZE_MAX) if (max_size < FIV_THUMBNAIL_SIZE_MIN || max_size > FIV_THUMBNAIL_SIZE_MAX)
return orient_thumbnail(surface); return fiv_io_image_to_surface(orient_thumbnail(image));
cairo_surface_t *result = FivIoImage *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size); adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return result; return fiv_io_image_to_surface(result);
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static WebPData static WebPData
encode_thumbnail(cairo_surface_t *surface) encode_thumbnail(FivIoImage *image)
{ {
WebPData bitstream = {}; WebPData bitstream = {};
WebPConfig config = {}; WebPConfig config = {};
@@ -539,12 +570,12 @@ encode_thumbnail(cairo_surface_t *surface)
if (!WebPValidateConfig(&config)) if (!WebPValidateConfig(&config))
return bitstream; return bitstream;
bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size); bitstream.bytes = fiv_io_encode_webp(image, &config, &bitstream.size);
return bitstream; return bitstream;
} }
static void static void
save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum) save_thumbnail(FivIoImage *thumbnail, const char *path, GString *thum)
{ {
WebPMux *mux = WebPMuxNew(); WebPMux *mux = WebPMuxNew();
WebPData bitstream = encode_thumbnail(thumbnail); WebPData bitstream = encode_thumbnail(thumbnail);
@@ -600,15 +631,15 @@ fiv_thumbnail_produce_for_search(
return NULL; return NULL;
gboolean color_managed = FALSE; gboolean color_managed = FALSE;
cairo_surface_t *surface = render(target, data, &color_managed, error); FivIoImage *image = render(target, data, &color_managed, error);
if (!surface) if (!image)
return NULL; return NULL;
// TODO(p): Might want to keep this a square. // TODO(p): Might want to keep this a square.
cairo_surface_t *result = FivIoImage *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size); adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return result; return fiv_io_image_to_surface(result);
} }
static cairo_surface_t * static cairo_surface_t *
@@ -636,14 +667,14 @@ produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
return NULL; return NULL;
gboolean color_managed = FALSE; gboolean color_managed = FALSE;
cairo_surface_t *surface = render(target, data, &color_managed, error); FivIoImage *image = render(target, data, &color_managed, error);
if (!surface) if (!image)
return NULL; return NULL;
cairo_surface_t *result = FivIoImage *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[size].size); adjust_thumbnail(image, fiv_thumbnail_sizes[size].size);
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return result; return fiv_io_image_to_surface(result);
} }
cairo_surface_t * cairo_surface_t *
@@ -688,10 +719,10 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
} }
gboolean color_managed = FALSE; gboolean color_managed = FALSE;
cairo_surface_t *surface = FivIoImage *image =
render(target, g_mapped_file_get_bytes(mf), &color_managed, error); render(target, g_mapped_file_get_bytes(mf), &color_managed, error);
g_mapped_file_unref(mf); g_mapped_file_unref(mf);
if (!surface) if (!image)
return NULL; return NULL;
// Boilerplate copied from fiv_thumbnail_lookup(). // Boilerplate copied from fiv_thumbnail_lookup().
@@ -707,12 +738,10 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_string_append_printf( g_string_append_printf(
thum, "%s%c%llu%c", THUMB_SIZE, 0, (unsigned long long) filesize, 0); thum, "%s%c%llu%c", THUMB_SIZE, 0, (unsigned long long) filesize, 0);
if (cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE) { g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_WIDTH, 0,
g_string_append_printf(thum, "%s%c%d%c", THUMB_IMAGE_WIDTH, 0, (unsigned) image->width, 0);
cairo_image_surface_get_width(surface), 0); g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_HEIGHT, 0,
g_string_append_printf(thum, "%s%c%d%c", THUMB_IMAGE_HEIGHT, 0, (unsigned) image->height, 0);
cairo_image_surface_get_height(surface), 0);
}
// Without a CMM, no conversion is attempted. // Without a CMM, no conversion is attempted.
if (color_managed) { if (color_managed) {
@@ -720,19 +749,19 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
thum, "%s%c%s%c", THUMB_COLORSPACE, 0, THUMB_COLORSPACE_SRGB, 0); thum, "%s%c%s%c", THUMB_COLORSPACE, 0, THUMB_COLORSPACE_SRGB, 0);
} }
cairo_surface_t *max_size_surface = NULL; FivIoImage *max_size_image = NULL;
for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) { for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) {
cairo_surface_t *scaled = FivIoImage *scaled =
adjust_thumbnail(surface, fiv_thumbnail_sizes[use].size); adjust_thumbnail(image, fiv_thumbnail_sizes[use].size);
gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir, gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir,
fiv_thumbnail_sizes[use].thumbnail_spec_name, sum); fiv_thumbnail_sizes[use].thumbnail_spec_name, sum);
save_thumbnail(scaled, path, thum); save_thumbnail(scaled, path, thum);
g_free(path); g_free(path);
if (!max_size_surface) if (!max_size_image)
max_size_surface = scaled; max_size_image = scaled;
else else
cairo_surface_destroy(scaled); fiv_io_image_unref(scaled);
} }
g_string_free(thum, TRUE); g_string_free(thum, TRUE);
@@ -740,8 +769,8 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_free(thumbnails_dir); g_free(thumbnails_dir);
g_free(sum); g_free(sum);
g_free(uri); g_free(uri);
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return max_size_surface; return fiv_io_image_to_surface(max_size_image);
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -791,23 +820,23 @@ read_wide_thumbnail(const char *path, const Stat *st, GError **error)
if (!thumbnail_uri) if (!thumbnail_uri)
return NULL; return NULL;
cairo_surface_t *surface = FivIoImage *image =
fiv_io_open(&(FivIoOpenContext){.uri = thumbnail_uri}, error); fiv_io_open(&(FivIoOpenContext){.uri = thumbnail_uri}, error);
g_free(thumbnail_uri); g_free(thumbnail_uri);
if (!surface) if (!image)
return NULL; return NULL;
bool sRGB = false; bool sRGB = false;
GBytes *thum = cairo_surface_get_user_data(surface, &fiv_io_key_thum); if (!image->thum) {
if (!thum) {
g_clear_error(error); g_clear_error(error);
set_error(error, "not a thumbnail"); set_error(error, "not a thumbnail");
} else if (!check_wide_thumbnail_texts(thum, st, &sRGB)) { } else if (!check_wide_thumbnail_texts(image->thum, st, &sRGB)) {
g_clear_error(error); g_clear_error(error);
set_error(error, "mismatch"); set_error(error, "mismatch");
} else { } else {
// TODO(p): Add a function or a non-valueless define to check // TODO(p): Add a function or a non-valueless define to check
// for CMM presence, then remove this ifdef. // for CMM presence, then remove this ifdef.
cairo_surface_t *surface = fiv_io_image_to_surface(image);
#ifdef HAVE_LCMS2 #ifdef HAVE_LCMS2
if (!sRGB) if (!sRGB)
mark_thumbnail_lq(surface); mark_thumbnail_lq(surface);
@@ -815,21 +844,21 @@ read_wide_thumbnail(const char *path, const Stat *st, GError **error)
return surface; return surface;
} }
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return NULL; return NULL;
} }
static cairo_surface_t * static cairo_surface_t *
read_png_thumbnail(const char *path, const Stat *st, GError **error) read_png_thumbnail(const char *path, const Stat *st, GError **error)
{ {
cairo_surface_t *surface = fiv_io_open_png_thumbnail(path, error); FivIoImage *image = fiv_io_open_png_thumbnail(path, error);
if (!surface) if (!image)
return NULL; return NULL;
GHashTable *texts = cairo_surface_get_user_data(surface, &fiv_io_key_text); GHashTable *texts = image->text;
if (!texts) { if (!texts) {
set_error(error, "not a thumbnail"); set_error(error, "not a thumbnail");
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return NULL; return NULL;
} }
@@ -841,16 +870,16 @@ read_png_thumbnail(const char *path, const Stat *st, GError **error)
if (!text_uri || strcmp(text_uri, st->uri) || if (!text_uri || strcmp(text_uri, st->uri) ||
!text_mtime || atol(text_mtime) != st->mtime) { !text_mtime || atol(text_mtime) != st->mtime) {
set_error(error, "mismatch or not a thumbnail"); set_error(error, "mismatch or not a thumbnail");
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return NULL; return NULL;
} }
if (text_size && strtoull(text_size, NULL, 10) != st->size) { if (text_size && strtoull(text_size, NULL, 10) != st->size) {
set_error(error, "file size mismatch"); set_error(error, "file size mismatch");
cairo_surface_destroy(surface); fiv_io_image_unref(image);
return NULL; return NULL;
} }
return surface; return fiv_io_image_to_surface(image);
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View File

@@ -1,7 +1,7 @@
// //
// fiv-view.c: image viewing widget // fiv-view.c: image viewing widget
// //
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name> // Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
// //
// Permission to use, copy, modify, and/or distribute this software for any // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // purpose with or without fee is hereby granted.
@@ -24,6 +24,7 @@
#include <math.h> #include <math.h>
#include <stdbool.h> #include <stdbool.h>
#include <epoxy/gl.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11 #ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h> #include <gdk/gdkx.h>
@@ -63,10 +64,10 @@ struct _FivView {
gchar *messages; ///< Image load information gchar *messages; ///< Image load information
gchar *uri; ///< Path to the current image (if any) gchar *uri; ///< Path to the current image (if any)
cairo_surface_t *image; ///< The loaded image (sequence) FivIoImage *image; ///< The loaded image (sequence)
cairo_surface_t *page; ///< Current page within image, weak FivIoImage *page; ///< Current page within image, weak
cairo_surface_t *page_scaled; ///< Current page within image, scaled FivIoImage *page_scaled; ///< Current page within image, scaled
cairo_surface_t *frame; ///< Current frame within page, weak FivIoImage *frame; ///< Current frame within page, weak
FivIoOrientation orientation; ///< Current page orientation FivIoOrientation orientation; ///< Current page orientation
bool enable_cms : 1; ///< Smooth scaling toggle bool enable_cms : 1; ///< Smooth scaling toggle
bool filter : 1; ///< Smooth scaling toggle bool filter : 1; ///< Smooth scaling toggle
@@ -77,12 +78,16 @@ struct _FivView {
double scale; ///< Scaling factor double scale; ///< Scaling factor
double drag_start[2]; ///< Adjustment values for drag origin double drag_start[2]; ///< Adjustment values for drag origin
cairo_surface_t *enhance_swap; ///< Quick swap in/out FivIoImage *enhance_swap; ///< Quick swap in/out
FivIoProfile screen_cms_profile; ///< Target colour profile for widget FivIoProfile *screen_cms_profile; ///< Target colour profile for widget
int remaining_loops; ///< Greater than zero if limited int remaining_loops; ///< Greater than zero if limited
gint64 frame_time; ///< Current frame's start, µs precision gint64 frame_time; ///< Current frame's start, µs precision
gulong frame_update_connection; ///< GdkFrameClock::update gulong frame_update_connection; ///< GdkFrameClock::update
GdkGLContext *gl_context; ///< OpenGL context
bool gl_initialized; ///< Objects have been created
GLuint gl_program; ///< Linked render program
}; };
G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0, G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0,
@@ -161,6 +166,147 @@ enum {
// Globals are, sadly, the canonical way of storing signal numbers. // Globals are, sadly, the canonical way of storing signal numbers.
static guint view_signals[LAST_SIGNAL]; static guint view_signals[LAST_SIGNAL];
// --- OpenGL ------------------------------------------------------------------
// While GTK+ 3 technically still supports legacy desktop OpenGL 2.0[1],
// we will pick the 3.3 core profile, which is fairly old by now.
// It doesn't seem to make any sense to go below 3.2.
//
// [1] https://stackoverflow.com/a/37923507/76313
//
// OpenGL ES
//
// Currently, we do not support OpenGL ES at all--it needs its own shaders
// (if only because of different #version statements), and also further analysis
// as to what is our minimum version requirement. While GTK+ 3 can again go
// down as low as OpenGL ES 2.0, this might be too much of a hassle to support.
//
// ES can be forced via GDK_GL=gles, if gdk_gl_context_set_required_version()
// doesn't stand in the way.
//
// Let's not forget that this is a desktop image viewer first and foremost.
static const char *
gl_error_string(GLenum err)
{
switch (err) {
case GL_NO_ERROR:
return "no error";
case GL_CONTEXT_LOST:
return "context lost";
case GL_INVALID_ENUM:
return "invalid enum";
case GL_INVALID_VALUE:
return "invalid value";
case GL_INVALID_OPERATION:
return "invalid operation";
case GL_INVALID_FRAMEBUFFER_OPERATION:
return "invalid framebuffer operation";
case GL_OUT_OF_MEMORY:
return "out of memory";
case GL_STACK_UNDERFLOW:
return "stack underflow";
case GL_STACK_OVERFLOW:
return "stack overflow";
default:
return NULL;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *gl_vertex =
"#version 330\n"
"layout(location = 0) in vec4 position;\n"
"out vec2 coordinates;\n"
"void main() {\n"
"\tcoordinates = position.zw;\n"
"\tgl_Position = vec4(position.xy, 0., 1.);\n"
"}\n";
static const char *gl_fragment =
"#version 330\n"
"in vec2 coordinates;\n"
"layout(location = 0) out vec4 color;\n"
"uniform sampler2D picture;\n"
"uniform bool checkerboard;\n"
"\n"
"vec3 checker() {\n"
"\tvec2 xy = gl_FragCoord.xy / 20.;\n"
"\tif (checkerboard && (int(floor(xy.x) + floor(xy.y)) & 1) == 0)\n"
"\t\treturn vec3(0.98);\n"
"\telse\n"
"\t\treturn vec3(1.00);\n"
"}\n"
"\n"
"void main() {\n"
"\tvec3 c = checker();\n"
"\tvec4 t = texture(picture, coordinates);\n"
"\t// Premultiplied blending with a solid background.\n"
"\t// XXX: This is only correct for linear components.\n"
"\tcolor = vec4(c * (1. - t.a) + t.rgb, 1.);\n"
"}\n";
static GLuint
gl_make_shader(int type, const char *glsl)
{
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &glsl, NULL);
glCompileShader(shader);
GLint status = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (!status) {
GLint len = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
GLchar *buffer = g_malloc0(len + 1);
glGetShaderInfoLog(shader, len, NULL, buffer);
g_warning("GL shader compilation failed: %s", buffer);
g_free(buffer);
glDeleteShader(shader);
return 0;
}
return shader;
}
static GLuint
gl_make_program(void)
{
GLuint vertex = gl_make_shader(GL_VERTEX_SHADER, gl_vertex);
GLuint fragment = gl_make_shader(GL_FRAGMENT_SHADER, gl_fragment);
if (!vertex || !fragment) {
glDeleteShader(vertex);
glDeleteShader(fragment);
return 0;
}
GLuint program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
glDeleteShader(vertex);
glDeleteShader(fragment);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (!status) {
GLint len = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
GLchar *buffer = g_malloc0(len + 1);
glGetProgramInfoLog(program, len, NULL, buffer);
g_warning("GL program linking failed: %s", buffer);
g_free(buffer);
glDeleteProgram(program);
return 0;
}
return program;
}
// -----------------------------------------------------------------------------
static void static void
on_adjustment_value_changed( on_adjustment_value_changed(
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data) G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
@@ -198,12 +344,14 @@ update_adjustments(FivView *self)
if (self->hadjustment) { if (self->hadjustment) {
gtk_adjustment_configure(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); alloc.width * 0.1, alloc.width * 0.9, alloc.width);
} }
if (self->vadjustment) { if (self->vadjustment) {
gtk_adjustment_configure(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); alloc.height * 0.1, alloc.height * 0.9, alloc.height);
} }
} }
@@ -234,9 +382,9 @@ fiv_view_finalize(GObject *gobject)
{ {
FivView *self = FIV_VIEW(gobject); FivView *self = FIV_VIEW(gobject);
g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free); g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free);
g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
g_clear_pointer(&self->image, cairo_surface_destroy); g_clear_pointer(&self->image, fiv_io_image_unref);
g_clear_pointer(&self->page_scaled, cairo_surface_destroy); g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
g_free(self->uri); g_free(self->uri);
g_free(self->messages); g_free(self->messages);
@@ -283,15 +431,13 @@ fiv_view_get_property(
g_value_set_boolean(value, !!self->image); g_value_set_boolean(value, !!self->image);
break; break;
case PROP_CAN_ANIMATE: case PROP_CAN_ANIMATE:
g_value_set_boolean(value, self->page && g_value_set_boolean(value, self->page && self->page->frame_next);
cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next));
break; break;
case PROP_HAS_PREVIOUS_PAGE: case PROP_HAS_PREVIOUS_PAGE:
g_value_set_boolean(value, self->image && self->page != self->image); g_value_set_boolean(value, self->image && self->page != self->image);
break; break;
case PROP_HAS_NEXT_PAGE: case PROP_HAS_NEXT_PAGE:
g_value_set_boolean(value, self->page && g_value_set_boolean(value, self->page && self->page->page_next);
cairo_surface_get_user_data(self->page, &fiv_io_key_page_next));
break; break;
case PROP_HADJUSTMENT: case PROP_HADJUSTMENT:
@@ -403,20 +549,34 @@ static void
prescale_page(FivView *self) prescale_page(FivView *self)
{ {
FivIoRenderClosure *closure = NULL; FivIoRenderClosure *closure = NULL;
if (!self->image || !(closure = if (!self->image || !(closure = self->page->render))
cairo_surface_get_user_data(self->page, &fiv_io_key_render)))
return; return;
// TODO(p): Restart the animation. No vector formats currently animate. // TODO(p): Restart the animation. No vector formats currently animate.
g_return_if_fail(!self->frame_update_connection); 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. // If it fails, the previous frame pointer may become invalid.
g_clear_pointer(&self->page_scaled, cairo_surface_destroy); 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) if (!self->page_scaled)
self->frame = self->page; self->frame = self->page;
} }
static void
set_source_image(FivView *self, cairo_t *cr)
{
cairo_surface_t *surface = fiv_io_image_to_surface_noref(self->frame);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
}
static void static void
fiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation) fiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
{ {
@@ -448,6 +608,27 @@ out:
// //
// Note that Wayland does not have any appropriate protocol, as of writing: // Note that Wayland does not have any appropriate protocol, as of writing:
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14 // https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
static FivIoProfile *
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 static void
reload_screen_cms_profile(FivView *self, GdkWindow *window) reload_screen_cms_profile(FivView *self, GdkWindow *window)
{ {
@@ -465,7 +646,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
gchar *data = NULL; gchar *data = NULL;
gsize length = 0; gsize length = 0;
if (g_file_get_contents(path, &data, &length, NULL)) if (g_file_get_contents(path, &data, &length, NULL))
self->screen_cms_profile = fiv_io_profile_new(data, length); self->screen_cms_profile = fiv_io_cmm_get_profile(
fiv_io_cmm_get_default(), data, length);
g_free(data); g_free(data);
} }
g_free(path); g_free(path);
@@ -477,6 +659,7 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
GdkDisplay *display = gdk_window_get_display(window); GdkDisplay *display = gdk_window_get_display(window);
GdkMonitor *monitor = gdk_display_get_monitor_at_window(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; int num = -1;
for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; ) for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; )
@@ -485,24 +668,14 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
if (num < 0) if (num < 0)
goto out; goto out;
char atom[32] = ""; // Cater to xiccd limitations (agalakhov/xiccd#33).
g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num); if (!(self->screen_cms_profile = monitor_cms_profile(root, num)) && num)
self->screen_cms_profile = monitor_cms_profile(root, 0);
// 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);
}
out: out:
if (!self->screen_cms_profile) if (!self->screen_cms_profile)
self->screen_cms_profile = fiv_io_profile_new_sRGB(); self->screen_cms_profile =
fiv_io_cmm_get_profile_sRGB(fiv_io_cmm_get_default());
} }
static void static void
@@ -536,6 +709,9 @@ fiv_view_realize(GtkWidget *widget)
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget), GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); &attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
gboolean opengl = g_settings_get_boolean(settings, "opengl");
// Without the following call, or the rendering mode set to "recording", // Without the following call, or the rendering mode set to "recording",
// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal() // RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
// creates backing stores using cairo_content_t constants. // creates backing stores using cairo_content_t constants.
@@ -545,19 +721,274 @@ fiv_view_realize(GtkWidget *widget)
// Note that this disables double buffering, and sometimes causes artefacts, // Note that this disables double buffering, and sometimes causes artefacts,
// see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560 // see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
// //
// If GTK+'s OpenGL integration fails to deliver, we need to use the window // GTK+'s OpenGL integration is terrible, so we may need to use
// directly, sidestepping the toolkit entirely. // the X11 subwindow directly, sidestepping the toolkit entirely.
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
if (GDK_IS_X11_WINDOW(window) && if (GDK_IS_X11_WINDOW(window) &&
g_settings_get_boolean(settings, "native-view-window")) g_settings_get_boolean(settings, "native-view-window"))
gdk_window_ensure_native(window); gdk_window_ensure_native(window);
#endif // GDK_WINDOWING_X11 #endif // GDK_WINDOWING_X11
g_object_unref(settings);
gtk_widget_register_window(widget, window); gtk_widget_register_window(widget, window);
gtk_widget_set_window(widget, window); gtk_widget_set_window(widget, window);
gtk_widget_set_realized(widget, TRUE); gtk_widget_set_realized(widget, TRUE);
reload_screen_cms_profile(FIV_VIEW(widget), window); reload_screen_cms_profile(FIV_VIEW(widget), window);
FivView *self = FIV_VIEW(widget);
g_clear_object(&self->gl_context);
if (!opengl)
return;
GError *error = NULL;
GdkGLContext *gl_context = gdk_window_create_gl_context(window, &error);
if (!gl_context) {
g_warning("GL: %s", error->message);
g_error_free(error);
return;
}
gdk_gl_context_set_use_es(gl_context, FALSE);
gdk_gl_context_set_required_version(gl_context, 3, 3);
gdk_gl_context_set_debug_enabled(gl_context, TRUE);
if (!gdk_gl_context_realize(gl_context, &error)) {
g_warning("GL: %s", error->message);
g_error_free(error);
g_object_unref(gl_context);
return;
}
self->gl_context = gl_context;
}
static void GLAPIENTRY
gl_on_message(G_GNUC_UNUSED GLenum source, GLenum type, G_GNUC_UNUSED GLuint id,
G_GNUC_UNUSED GLenum severity, G_GNUC_UNUSED GLsizei length,
const GLchar *message, G_GNUC_UNUSED const void *user_data)
{
if (type == GL_DEBUG_TYPE_ERROR)
g_warning("GL: error: %s", message);
else
g_debug("GL: %s", message);
}
static void
fiv_view_unrealize(GtkWidget *widget)
{
FivView *self = FIV_VIEW(widget);
if (self->gl_context) {
if (self->gl_initialized) {
gdk_gl_context_make_current(self->gl_context);
glDeleteProgram(self->gl_program);
}
if (self->gl_context == gdk_gl_context_get_current())
gdk_gl_context_clear_current();
g_clear_object(&self->gl_context);
}
GTK_WIDGET_CLASS(fiv_view_parent_class)->unrealize(widget);
}
static bool
gl_draw(FivView *self, cairo_t *cr)
{
gdk_gl_context_make_current(self->gl_context);
if (!self->gl_initialized) {
GLuint program = gl_make_program();
if (!program)
return false;
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_BLEND);
if (epoxy_has_gl_extension("GL_ARB_debug_output")) {
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(gl_on_message, NULL);
}
self->gl_program = program;
self->gl_initialized = true;
}
// This limit is always less than that of Cairo/pixman,
// and we'd have to figure out tiling.
GLint max = 0;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
if (max < (GLint) self->frame->width ||
max < (GLint) self->frame->height) {
g_warning("OpenGL max. texture size is too small");
return false;
}
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
int dw = 0, dh = 0, dx = 0, dy = 0;
get_display_dimensions(self, &dw, &dh);
int clipw = dw, cliph = dh;
double x1 = 0., y1 = 0., x2 = 1., y2 = 1.;
if (self->hadjustment)
x1 = floor(gtk_adjustment_get_value(self->hadjustment)) / dw;
if (self->vadjustment)
y1 = floor(gtk_adjustment_get_value(self->vadjustment)) / dh;
if (dw <= allocation.width) {
dx = round((allocation.width - dw) / 2.);
} else {
x2 = x1 + (double) allocation.width / dw;
clipw = allocation.width;
}
if (dh <= allocation.height) {
dy = round((allocation.height - dh) / 2.);
} else {
y2 = y1 + (double) allocation.height / dh;
cliph = allocation.height;
}
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self));
clipw *= scale;
cliph *= scale;
enum { SRC, DEST };
GLuint textures[2] = {};
glGenTextures(2, textures);
// https://stackoverflow.com/questions/25157306 0..1
// GL_TEXTURE_RECTANGLE seems kind-of useful
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
if (self->filter) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
// GL_UNPACK_ALIGNMENT is initially 4, which is fine for these.
// Texture swizzling is OpenGL 3.3.
if (self->frame->format == CAIRO_FORMAT_ARGB32) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
} else if (self->frame->format == CAIRO_FORMAT_RGB24) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
} else if (self->frame->format == CAIRO_FORMAT_RGB30) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV, self->frame->data);
} else {
g_warning("GL: unsupported bitmap format");
}
// GtkGLArea creates textures like this.
glBindTexture(GL_TEXTURE_2D, textures[DEST]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, clipw, cliph, 0, GL_BGRA,
GL_UNSIGNED_BYTE, NULL);
glViewport(0, 0, clipw, cliph);
GLuint vao = 0;
glGenVertexArrays(1, &vao);
GLuint frame_buffer = 0;
glGenFramebuffers(1, &frame_buffer);
glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);
glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[DEST], 0);
glClearColor(0., 0., 0., 1.);
glClear(GL_COLOR_BUFFER_BIT);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
g_warning("GL framebuffer status: %u", status);
glUseProgram(self->gl_program);
GLint position_location = glGetAttribLocation(
self->gl_program, "position");
GLint picture_location = glGetUniformLocation(
self->gl_program, "picture");
GLint checkerboard_location = glGetUniformLocation(
self->gl_program, "checkerboard");
glUniform1i(picture_location, 0);
glUniform1i(checkerboard_location, self->checkerboard);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
// Note that the Y axis is flipped in the table.
double vertices[][4] = {
{-1., -1., x1, y2},
{+1., -1., x2, y2},
{+1., +1., x2, y1},
{-1., +1., x1, y1},
};
cairo_matrix_t matrix = fiv_io_orientation_matrix(self->orientation, 1, 1);
cairo_matrix_transform_point(&matrix, &vertices[0][2], &vertices[0][3]);
cairo_matrix_transform_point(&matrix, &vertices[1][2], &vertices[1][3]);
cairo_matrix_transform_point(&matrix, &vertices[2][2], &vertices[2][3]);
cairo_matrix_transform_point(&matrix, &vertices[3][2], &vertices[3][3]);
GLuint vertex_buffer = 0;
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
glBindVertexArray(vao);
glVertexAttribPointer(position_location,
G_N_ELEMENTS(vertices[0]), GL_DOUBLE, GL_FALSE, sizeof vertices[0], 0);
glEnableVertexAttribArray(position_location);
glDrawArrays(GL_TRIANGLE_FAN, 0, G_N_ELEMENTS(vertices));
glDisableVertexAttribArray(position_location);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glUseProgram(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// XXX: Native GdkWindows send this to the software fallback path.
// XXX: This only reliably alpha blends when using the software fallback,
// such as with a native window, because 7237f5d in GTK+ 3 is a regression.
// (Introduced in 3.24.39, reverted in 3.24.42.)
//
// We had to resort to rendering the checkerboard pattern in the shader.
// Unfortunately, it is hard to retrieve the theme colours from CSS.
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
cairo_translate(cr, dx, dy);
gdk_cairo_draw_from_gl(
cr, window, textures[DEST], GL_TEXTURE, scale, 0, 0, clipw, cliph);
gdk_gl_context_make_current(self->gl_context);
glDeleteBuffers(1, &vertex_buffer);
glDeleteTextures(2, textures);
glDeleteVertexArrays(1, &vao);
glDeleteFramebuffers(1, &frame_buffer);
// TODO(p): Possibly use this clue as a hint to use Cairo rendering.
GLenum err = 0;
while ((err = glGetError()) != GL_NO_ERROR) {
const char *string = gl_error_string(err);
if (string)
g_warning("GL: error: %s", string);
else
g_warning("GL: error: %u", err);
}
gdk_gl_context_clear_current();
return true;
} }
static gboolean static gboolean
@@ -574,8 +1005,10 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
if (!self->image || if (!self->image ||
!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget))) !gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
return TRUE; return TRUE;
if (self->gl_context && gl_draw(self, cr))
return TRUE;
int dw, dh; int dw = 0, dh = 0;
get_display_dimensions(self, &dw, &dh); get_display_dimensions(self, &dw, &dh);
double x = 0; double x = 0;
@@ -606,37 +1039,19 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
// Then all frames are pre-scaled. // Then all frames are pre-scaled.
if (self->page_scaled) { if (self->page_scaled) {
cairo_set_source_surface(cr, self->frame, 0, 0); set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr); cairo_paint(cr);
return TRUE; return TRUE;
} }
// FIXME: Recording surfaces do not work well with CAIRO_SURFACE_TYPE_XLIB,
// we always get a shitty pixmap, where transparency contains junk.
if (cairo_surface_get_type(self->frame) == CAIRO_SURFACE_TYPE_RECORDING) {
cairo_surface_t *image =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw, dh);
cairo_t *tcr = cairo_create(image);
cairo_scale(tcr, self->scale, self->scale);
cairo_set_source_surface(tcr, self->frame, 0, 0);
cairo_pattern_set_matrix(cairo_get_source(tcr), &matrix);
cairo_paint(tcr);
cairo_destroy(tcr);
cairo_set_source_surface(cr, image, 0, 0);
cairo_paint(cr);
cairo_surface_destroy(image);
return TRUE;
}
// XXX: The rounding together with padding may result in up to // XXX: The rounding together with padding may result in up to
// a pixel's worth of made-up picture data. // a pixel's worth of made-up picture data.
cairo_rectangle(cr, 0, 0, dw, dh); cairo_rectangle(cr, 0, 0, dw, dh);
cairo_clip(cr); cairo_clip(cr);
cairo_scale(cr, self->scale, self->scale); cairo_scale(cr, self->scale, self->scale);
cairo_set_source_surface(cr, self->frame, 0, 0); set_source_image(self, cr);
cairo_pattern_t *pattern = cairo_get_source(cr); cairo_pattern_t *pattern = cairo_get_source(cr);
cairo_pattern_set_matrix(pattern, &matrix); cairo_pattern_set_matrix(pattern, &matrix);
@@ -810,15 +1225,13 @@ stop_animating(FivView *self)
self->frame_time = 0; self->frame_time = 0;
self->frame_update_connection = 0; self->frame_update_connection = 0;
self->remaining_loops = 0;
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
} }
static gboolean static gboolean
advance_frame(FivView *self) advance_frame(FivView *self)
{ {
cairo_surface_t *next = FivIoImage *next = self->frame->frame_next;
cairo_surface_get_user_data(self->frame, &fiv_io_key_frame_next);
if (next) { if (next) {
self->frame = next; self->frame = next;
} else { } else {
@@ -836,8 +1249,7 @@ advance_animation(FivView *self, GdkFrameClock *clock)
gint64 now = gdk_frame_clock_get_frame_time(clock); gint64 now = gdk_frame_clock_get_frame_time(clock);
while (true) { while (true) {
// TODO(p): See if infinite frames can actually happen, and how. // TODO(p): See if infinite frames can actually happen, and how.
intptr_t duration = (intptr_t) cairo_surface_get_user_data( int64_t duration = self->frame->frame_duration;
self->frame, &fiv_io_key_frame_duration);
if (duration < 0) if (duration < 0)
return FALSE; return FALSE;
@@ -875,32 +1287,43 @@ start_animating(FivView *self)
stop_animating(self); stop_animating(self);
GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self)); GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self));
if (!clock || !self->image || if (!clock || !self->image || !self->page->frame_next)
!cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next))
return; return;
self->frame_time = gdk_frame_clock_get_frame_time(clock); self->frame_time = gdk_frame_clock_get_frame_time(clock);
self->frame_update_connection = g_signal_connect( self->frame_update_connection = g_signal_connect(
clock, "update", G_CALLBACK(on_frame_clock_update), self); clock, "update", G_CALLBACK(on_frame_clock_update), self);
self->remaining_loops =
(uintptr_t) cairo_surface_get_user_data(self->page, &fiv_io_key_loops); // Only restart looping the animation if it has stopped at the end.
if (!self->remaining_loops) {
self->remaining_loops = self->page->loops;
if (self->remaining_loops && !self->frame->frame_next) {
self->frame = self->page;
gtk_widget_queue_draw(GTK_WIDGET(self));
}
}
gdk_frame_clock_begin_updating(clock); gdk_frame_clock_begin_updating(clock);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
} }
static void static void
switch_page(FivView *self, cairo_surface_t *page) switch_page(FivView *self, FivIoImage *page)
{ {
g_clear_pointer(&self->page_scaled, cairo_surface_destroy); g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
self->frame = self->page = page; 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); prescale_page(self);
if (!self->page || if (!self->page ||
(self->orientation = (uintptr_t) cairo_surface_get_user_data( (self->orientation = self->page->orientation) ==
self->page, &fiv_io_key_orientation)) == FivIoOrientationUnknown) FivIoOrientationUnknown)
self->orientation = FivIoOrientation0; self->orientation = FivIoOrientation0;
self->remaining_loops = 0;
start_animating(self); start_animating(self);
gtk_widget_queue_resize(GTK_WIDGET(self)); gtk_widget_queue_resize(GTK_WIDGET(self));
@@ -1027,7 +1450,7 @@ copy(FivView *self)
cairo_surface_t *transformed = cairo_surface_t *transformed =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
cairo_t *cr = cairo_create(transformed); cairo_t *cr = cairo_create(transformed);
cairo_set_source_surface(cr, self->frame, 0, 0); set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr); cairo_paint(cr);
cairo_destroy(cr); cairo_destroy(cr);
@@ -1065,7 +1488,7 @@ on_draw_page(G_GNUC_UNUSED GtkPrintOperation *operation,
cairo_t *cr = gtk_print_context_get_cairo_context(context); cairo_t *cr = gtk_print_context_get_cairo_context(context);
cairo_scale(cr, scale, scale); cairo_scale(cr, scale, scale);
cairo_set_source_surface(cr, self->frame, 0, 0); set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr); cairo_paint(cr);
} }
@@ -1100,10 +1523,10 @@ print(FivView *self)
} }
static gboolean static gboolean
save_as(FivView *self, cairo_surface_t *frame) save_as(FivView *self, FivIoImage *frame)
{ {
GtkWindow *window = get_toplevel(GTK_WIDGET(self)); GtkWindow *window = get_toplevel(GTK_WIDGET(self));
FivIoProfile target = NULL; FivIoProfile *target = NULL;
if (self->enable_cms && (target = self->screen_cms_profile)) { if (self->enable_cms && (target = self->screen_cms_profile)) {
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL, GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s", GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
@@ -1279,6 +1702,7 @@ fiv_view_class_init(FivViewClass *klass)
widget_class->map = fiv_view_map; widget_class->map = fiv_view_map;
widget_class->unmap = fiv_view_unmap; widget_class->unmap = fiv_view_unmap;
widget_class->realize = fiv_view_realize; widget_class->realize = fiv_view_realize;
widget_class->unrealize = fiv_view_unrealize;
widget_class->draw = fiv_view_draw; widget_class->draw = fiv_view_draw;
widget_class->button_press_event = fiv_view_button_press_event; widget_class->button_press_event = fiv_view_button_press_event;
widget_class->scroll_event = fiv_view_scroll_event; widget_class->scroll_event = fiv_view_scroll_event;
@@ -1362,11 +1786,12 @@ fiv_view_init(FivView *self)
// --- Public interface -------------------------------------------------------- // --- Public interface --------------------------------------------------------
static cairo_surface_t * static FivIoImage *
open_without_swapping_in(FivView *self, const char *uri) open_without_swapping_in(FivView *self, const char *uri)
{ {
FivIoOpenContext ctx = { FivIoOpenContext ctx = {
.uri = uri, .uri = uri,
.cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL,
.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL, .screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
.screen_dpi = 96, // TODO(p): Try to retrieve it from the screen. .screen_dpi = 96, // TODO(p): Try to retrieve it from the screen.
.enhance = self->enhance, .enhance = self->enhance,
@@ -1374,7 +1799,7 @@ open_without_swapping_in(FivView *self, const char *uri)
}; };
GError *error = NULL; GError *error = NULL;
cairo_surface_t *surface = fiv_io_open(&ctx, &error); FivIoImage *image = fiv_io_open(&ctx, &error);
if (error) { if (error) {
g_ptr_array_add(ctx.warnings, g_strdup(error->message)); g_ptr_array_add(ctx.warnings, g_strdup(error->message));
g_error_free(error); g_error_free(error);
@@ -1387,7 +1812,7 @@ open_without_swapping_in(FivView *self, const char *uri)
} }
g_ptr_array_free(ctx.warnings, TRUE); g_ptr_array_free(ctx.warnings, TRUE);
return surface; return image;
} }
// TODO(p): Progressive picture loading, or at least async/cancellable. // TODO(p): Progressive picture loading, or at least async/cancellable.
@@ -1395,18 +1820,18 @@ gboolean
fiv_view_set_uri(FivView *self, const char *uri) fiv_view_set_uri(FivView *self, const char *uri)
{ {
// This is extremely expensive, and only works sometimes. // This is extremely expensive, and only works sometimes.
g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
if (self->enhance) { if (self->enhance) {
self->enhance = FALSE; self->enhance = FALSE;
g_object_notify_by_pspec( g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_ENHANCE]); G_OBJECT(self), view_properties[PROP_ENHANCE]);
} }
cairo_surface_t *surface = open_without_swapping_in(self, uri); FivIoImage *image = open_without_swapping_in(self, uri);
g_clear_pointer(&self->image, cairo_surface_destroy); g_clear_pointer(&self->image, fiv_io_image_unref);
self->frame = self->page = NULL; self->frame = self->page = NULL;
self->image = surface; self->image = image;
switch_page(self, self->image); switch_page(self, self->image);
// Otherwise, adjustment values and zoom are retained implicitly. // Otherwise, adjustment values and zoom are retained implicitly.
@@ -1418,15 +1843,15 @@ fiv_view_set_uri(FivView *self, const char *uri)
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_HAS_IMAGE]); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_HAS_IMAGE]);
return surface != NULL; return image != NULL;
} }
static void static void
page_step(FivView *self, int step) page_step(FivView *self, int step)
{ {
cairo_user_data_key_t *key = FivIoImage *page = step < 0
step < 0 ? &fiv_io_key_page_previous : &fiv_io_key_page_next; ? self->page->page_previous
cairo_surface_t *page = cairo_surface_get_user_data(self->page, key); : self->page->page_next;
if (page) if (page)
switch_page(self, page); switch_page(self, page);
} }
@@ -1435,31 +1860,35 @@ static void
frame_step(FivView *self, int step) frame_step(FivView *self, int step)
{ {
stop_animating(self); stop_animating(self);
cairo_user_data_key_t *key =
step < 0 ? &fiv_io_key_frame_previous : &fiv_io_key_frame_next; if (step > 0) {
if (!step || !(self->frame = cairo_surface_get_user_data(self->frame, key))) // Decrease the loop counter as if running on a timer.
(void) advance_frame(self);
} else if (!step || !(self->frame = self->frame->frame_previous)) {
self->frame = self->page; self->frame = self->page;
self->remaining_loops = 0;
}
gtk_widget_queue_draw(GTK_WIDGET(self)); gtk_widget_queue_draw(GTK_WIDGET(self));
} }
static gboolean static gboolean
reload(FivView *self) reload(FivView *self)
{ {
cairo_surface_t *surface = open_without_swapping_in(self, self->uri); FivIoImage *image = open_without_swapping_in(self, self->uri);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]);
if (!surface) if (!image)
return FALSE; return FALSE;
g_clear_pointer(&self->image, cairo_surface_destroy); g_clear_pointer(&self->image, fiv_io_image_unref);
g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
switch_page(self, (self->image = surface)); switch_page(self, (self->image = image));
return TRUE; return TRUE;
} }
static void static void
swap_enhanced_image(FivView *self) swap_enhanced_image(FivView *self)
{ {
cairo_surface_t *saved = self->image; FivIoImage *saved = self->image;
self->image = self->page = self->frame = NULL; self->image = self->page = self->frame = NULL;
if (self->enhance_swap) { if (self->enhance_swap) {
@@ -1546,9 +1975,8 @@ fiv_view_command(FivView *self, FivViewCommand command)
break; case FIV_VIEW_COMMAND_PAGE_NEXT: break; case FIV_VIEW_COMMAND_PAGE_NEXT:
page_step(self, +1); page_step(self, +1);
break; case FIV_VIEW_COMMAND_PAGE_LAST: break; case FIV_VIEW_COMMAND_PAGE_LAST:
for (cairo_surface_t *s = self->page; for (FivIoImage *I = self->page; (I = I->page_next); )
(s = cairo_surface_get_user_data(s, &fiv_io_key_page_next)); ) self->page = I;
self->page = s;
switch_page(self, self->page); switch_page(self, self->page);
break; case FIV_VIEW_COMMAND_FRAME_FIRST: break; case FIV_VIEW_COMMAND_FRAME_FIRST:

474
fiv.c
View File

@@ -1,7 +1,7 @@
// //
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer // fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
// //
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name> // Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
// //
// Permission to use, copy, modify, and/or distribute this software for any // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // purpose with or without fee is hereby granted.
@@ -27,6 +27,7 @@
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#ifdef G_OS_WIN32 #ifdef G_OS_WIN32
#include <io.h> #include <io.h>
@@ -43,11 +44,6 @@
#include "fiv-thumbnail.h" #include "fiv-thumbnail.h"
#include "fiv-view.h" #include "fiv-view.h"
#ifdef HAVE_LCMS2_FAST_FLOAT
#include <lcms2.h>
#include <lcms2_fast_float.h>
#endif // HAVE_LCMS2_FAST_FLOAT
// --- Utilities --------------------------------------------------------------- // --- Utilities ---------------------------------------------------------------
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2); static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
@@ -100,16 +96,16 @@ struct key_section {
static struct key help_keys_general[] = { static struct key help_keys_general[] = {
{"F1", "Show help"}, {"F1", "Show help"},
{"F10", "Open menu"}, {"F10", "Open menu"},
{"<Control>comma", "Preferences"}, {"<Primary>comma", "Preferences"},
{"<Control>question", "Keyboard shortcuts"}, {"<Primary>question", "Keyboard shortcuts"},
{"q <Control>q", "Quit"}, {"q <Primary>q", "Quit"},
{"<Control>w", "Quit"}, {"<Primary>w", "Quit"},
{} {}
}; };
static struct key help_keys_navigation[] = { static struct key help_keys_navigation[] = {
{"<Control>l", "Open location..."}, {"<Primary>l", "Open location..."},
{"<Control>n", "Open a new window"}, {"<Primary>n", "Open a new window"},
{"<Alt>Left", "Go back in history"}, {"<Alt>Left", "Go back in history"},
{"<Alt>Right", "Go forward in history"}, {"<Alt>Right", "Go forward in history"},
{} {}
@@ -458,7 +454,7 @@ show_about_dialog(GtkWidget *parent)
GtkWidget *website = gtk_label_new(NULL); GtkWidget *website = gtk_label_new(NULL);
gtk_label_set_selectable(GTK_LABEL(website), TRUE); 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); gchar *link = g_strdup_printf("<a href='%s'>%s</a>", url, url);
gtk_label_set_markup(GTK_LABEL(website), link); gtk_label_set_markup(GTK_LABEL(website), link);
g_free(link); g_free(link);
@@ -514,6 +510,113 @@ show_about_dialog(GtkWidget *parent)
cairo_pattern_destroy(ctx.v_pattern); cairo_pattern_destroy(ctx.v_pattern);
} }
// --- Settings ----------------------------------------------------------------
static void
preferences_make_row(
GtkWidget *grid, int *row, GSettings *settings, GSettingsSchemaKey *key)
{
const char *name = g_settings_schema_key_get_name(key);
const char *summary = g_settings_schema_key_get_summary(key);
const char *description = g_settings_schema_key_get_description(key);
GtkWidget *widget = NULL;
const GVariantType *type = g_settings_schema_key_get_value_type(key);
if (g_variant_type_equal(type, G_VARIANT_TYPE_BOOLEAN)) {
widget = gtk_switch_new();
g_settings_bind(
settings, name, widget, "active", G_SETTINGS_BIND_DEFAULT);
} else {
const gchar *type = NULL;
GVariant *value = NULL, *range = g_settings_schema_key_get_range(key);
g_variant_get(range, "(&sv)", &type, &value);
GVariantIter iter = {};
g_variant_iter_init(&iter, value);
if (g_str_equal(type, "enum")) {
widget = gtk_combo_box_text_new();
GVariant *child = NULL;
while ((child = g_variant_iter_next_value(&iter))) {
const char *id = g_variant_get_string(child, NULL);
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(widget), id, id);
g_variant_unref(child);
}
g_settings_bind(
settings, name, widget, "active-id", G_SETTINGS_BIND_DEFAULT);
}
g_variant_unref(value);
g_variant_unref(range);
}
// Ignore unimplemented value types.
if (!widget)
return;
GtkWidget *label = gtk_label_new(summary ? summary : name);
gtk_label_set_xalign(GTK_LABEL(label), 0);
gtk_widget_set_hexpand(label, TRUE);
gtk_grid_attach(GTK_GRID(grid), label, 0, (*row), 1, 1);
gtk_widget_set_halign(widget, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), widget, 1, (*row)++, 1, 1);
if (description) {
GtkWidget *label = gtk_label_new(description);
PangoAttrList *attr_list = pango_attr_list_new();
pango_attr_list_insert(
attr_list, pango_attr_scale_new(PANGO_SCALE_SMALL));
gtk_label_set_attributes(
GTK_LABEL(label), pango_attr_list_ref(attr_list));
pango_attr_list_unref(attr_list);
gtk_label_set_xalign(GTK_LABEL(label), 0);
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
gtk_widget_set_sensitive(label, FALSE);
gtk_widget_set_size_request(label, 0, -1);
gtk_grid_attach(GTK_GRID(grid), label, 0, (*row)++, 1, 1);
}
}
static void
show_preferences(GtkWidget *parent)
{
GSettingsSchema *schema = NULL;
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
g_object_get(settings, "settings-schema", &schema, NULL);
GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
"use-header-bar", TRUE,
"title", "Preferences",
"transient-for", parent,
"destroy-with-parent", TRUE, NULL);
GtkWidget *grid = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(grid), 12);
gtk_grid_set_column_spacing(GTK_GRID(grid), 24);
g_object_set(grid, "margin", 12, NULL);
gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
grid, TRUE, TRUE, 0);
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);
}
g_strfreev(keys);
g_object_unref(settings);
gtk_window_set_default_size(GTK_WINDOW(dialog), 600, -1);
gtk_widget_show_all(dialog);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
}
// --- Main -------------------------------------------------------------------- // --- Main --------------------------------------------------------------------
// TODO(p): See if it's possible to give separators room to shrink // TODO(p): See if it's possible to give separators room to shrink
@@ -1344,20 +1447,6 @@ show_help_shortcuts(void)
gtk_widget_show(window); gtk_widget_show(window);
} }
static void
show_preferences(void)
{
char *argv[] = {"dconf-editor", PROJECT_NS PROJECT_NAME, NULL};
GError *error = NULL;
if (!g_spawn_async(
NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error)) {
if (g_error_matches(error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT))
g_prefix_error_literal(&error,
"Please install dconf-editor, or use the gsettings utility.\n");
show_error_dialog(error);
}
}
static void static void
toggle_sunlight(void) toggle_sunlight(void)
{ {
@@ -1368,89 +1457,17 @@ toggle_sunlight(void)
g_object_set(settings, property, !set, NULL); 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 static gboolean
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data) G_GNUC_UNUSED gpointer data)
{ {
switch (event->state & gtk_accelerator_get_default_mod_mask()) { 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:
case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
switch (event->keyval) { switch (event->keyval) {
case GDK_KEY_h: case GDK_KEY_h:
// XXX: Command-H is already occupied on macOS.
gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER])); gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
return TRUE; 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();
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_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;
} }
} }
@@ -1465,8 +1482,15 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
gtk_accelerator_parse(accelerator, &key, &mods); gtk_accelerator_parse(accelerator, &key, &mods);
g_free(accelerator); 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(); 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_widget_show(g.menu);
// _gtk_menu_shell_set_keyboard_mode() is private. // _gtk_menu_shell_set_keyboard_mode() is private.
@@ -1476,6 +1500,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
return FALSE; 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 static gboolean
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data) G_GNUC_UNUSED gpointer data)
@@ -1654,6 +1689,9 @@ make_toolbar_radio(const char *label, const char *tooltip)
GtkWidget *button = gtk_radio_button_new_with_label(NULL, label); GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
gtk_widget_set_tooltip_text(button, tooltip); gtk_widget_set_tooltip_text(button, tooltip);
gtk_widget_set_focus_on_click(button, FALSE); gtk_widget_set_focus_on_click(button, FALSE);
gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(button), FALSE);
gtk_style_context_add_class(
gtk_widget_get_style_context(button), GTK_STYLE_CLASS_FLAT);
return button; return button;
} }
@@ -1715,7 +1753,6 @@ make_browser_toolbar(void)
gtk_radio_button_join_group(radio, last); gtk_radio_button_join_group(radio, last);
last = radio; last = radio;
} }
return browser_toolbar; return browser_toolbar;
} }
@@ -1982,41 +2019,178 @@ make_browser_sidebar(FivIoModel *model)
return sidebar; return sidebar;
} }
static GtkWidget * // --- Actions -----------------------------------------------------------------
make_menu_bar(void)
#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"); static void
g_signal_connect_swapped(item_quit, "activate", set_up_action(GtkApplication *app, const ActionEntry *a)
G_CALLBACK(gtk_widget_destroy), g.window); {
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(); gchar *full_name = g_strdup_printf("app.%s", a->name);
gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit); gtk_application_set_accels_for_action(app, full_name, a->accels);
GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File"); g_free(full_name);
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_file), menu_file); }
gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_file);
GtkWidget *item_contents = gtk_menu_item_new_with_mnemonic("_Contents"); // --- Menu --------------------------------------------------------------------
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);
GtkWidget *menu_help = gtk_menu_new(); typedef struct {
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents); const char *label; ///< Label, with a mnemonic
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts); const char *action; ///< Prefixed action name
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about); gboolean macos; ///< Show in the macOS global menu?
GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help"); } MenuItem;
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 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. // 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_show_all(g.menu);
gtk_widget_set_no_show_all(g.menu, TRUE); gtk_widget_set_no_show_all(g.menu, TRUE);
gtk_widget_hide(g.menu); gtk_widget_hide(g.menu);
@@ -2024,6 +2198,8 @@ make_menu_bar(void)
return g.menu; return g.menu;
} }
// --- Application -------------------------------------------------------------
// This is incredibly broken https://stackoverflow.com/a/51054396/76313 // This is incredibly broken https://stackoverflow.com/a/51054396/76313
// thus resolving the problem using overlaps. // thus resolving the problem using overlaps.
// We're trying to be universal for light and dark themes both. It's hard. // We're trying to be universal for light and dark themes both. It's hard.
@@ -2032,7 +2208,10 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \ mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
fiv-view, fiv-browser { background: @content_view_bg; } \ fiv-view, fiv-browser { background: @content_view_bg; } \
placessidebar.fiv box > separator { margin: 4px 0; } \ placessidebar.fiv box > separator { margin: 4px 0; } \
placessidebar.fiv row { min-height: 2em; } \
.fiv-toolbar button { padding-left: 0; padding-right: 0; } \ .fiv-toolbar button { padding-left: 0; padding-right: 0; } \
.fiv-toolbar button.text-button { \
padding-left: 4px; padding-right: 4px; } \
.fiv-toolbar > button:first-child { padding-left: 4px; } \ .fiv-toolbar > button:first-child { padding-left: 4px; } \
.fiv-toolbar > button:last-child { padding-right: 4px; } \ .fiv-toolbar > button:last-child { padding-right: 4px; } \
.fiv-toolbar separator { \ .fiv-toolbar separator { \
@@ -2208,10 +2387,27 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
g_signal_connect(g.window, "window-state-event", g_signal_connect(g.window, "window-state-event",
G_CALLBACK(on_window_state_event), NULL); 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); 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(menu_box), g.stack);
gtk_container_add(GTK_CONTAINER(g.window), menu_box); gtk_container_add(GTK_CONTAINER(g.window), menu_box);
g_object_unref(menu);
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME); GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
if (g_settings_get_boolean(settings, "dark-theme")) if (g_settings_get_boolean(settings, "dark-theme"))
@@ -2256,7 +2452,7 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
} }
static struct { static struct {
gboolean browse, extract_thumbnail; gboolean browse, collection, extract_thumbnail;
gchar **args, *thumbnail_size, *thumbnail_size_search; gchar **args, *thumbnail_size, *thumbnail_size_search;
} o; } o;
@@ -2266,12 +2462,12 @@ on_app_activate(
{ {
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both // XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
// interpret multiple command line arguments differently, as a collection. // interpret multiple command line arguments differently, as a collection.
// However, single-element collections are unrepresentable this way. // However, single-element collections are unrepresentable this way,
// Should we allow multiple targets only in a special new mode? // so we have a switch to enforce it.
g.files_index = -1; g.files_index = -1;
if (o.args) { if (o.args) {
const gchar *target = *o.args; const gchar *target = *o.args;
if (o.args[1]) { if (o.args[1] || o.collection) {
fiv_collection_reload(o.args); fiv_collection_reload(o.args);
target = FIV_COLLECTION_SCHEME ":/"; target = FIV_COLLECTION_SCHEME ":/";
} }
@@ -2387,11 +2583,6 @@ on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
return 0; return 0;
} }
// TODO(p): Use Little CMS with contexts instead.
#ifdef HAVE_LCMS2_FAST_FLOAT
cmsPlugin(cmsFastFloatExtensions());
#endif // HAVE_LCMS2_FAST_FLOAT
// Normalize all arguments to URIs, and run thumbnailing modes first. // Normalize all arguments to URIs, and run thumbnailing modes first.
for (gsize i = 0; o.args && o.args[i]; i++) { for (gsize i = 0; o.args && o.args[i]; i++) {
GFile *resolved = g_file_new_for_commandline_arg(o.args[i]); GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
@@ -2422,6 +2613,9 @@ main(int argc, char *argv[])
{"browse", 0, G_OPTION_FLAG_IN_MAIN, {"browse", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &o.browse, G_OPTION_ARG_NONE, &o.browse,
"Start in filesystem browsing mode", NULL}, "Start in filesystem browsing mode", NULL},
{"collection", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &o.collection,
"Always put arguments in a collection (implies --browse)", NULL},
{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN, {"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, NULL, G_OPTION_ARG_NONE, NULL,
"Invalidate the wide thumbnail cache", NULL}, "Invalidate the wide thumbnail cache", NULL},

View File

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

71
fiv.wxs.in Normal file
View File

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

View File

@@ -1,7 +1,8 @@
# vim: noet ts=4 sts=4 sw=4: # vim: noet ts=4 sts=4 sw=4:
project('fiv', 'c', project('fiv', 'c',
default_options : ['c_std=gnu99', 'warning_level=2'], default_options : ['c_std=gnu99', 'warning_level=2'],
version : '0.1.0') version : '1.0.0',
meson_version : '>=0.57')
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
add_project_arguments( add_project_arguments(
@@ -35,7 +36,9 @@ gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
dependencies = [ dependencies = [
dependency('gtk+-3.0'), dependency('gtk+-3.0'),
dependency('pixman-1'), dependency('pixman-1'),
dependency('epoxy'),
dependency('libjpeg'),
dependency('libturbojpeg'), dependency('libturbojpeg'),
dependency('libwebp'), dependency('libwebp'),
dependency('libwebpdemux'), dependency('libwebpdemux'),
@@ -92,11 +95,13 @@ endif
# XXX: https://github.com/mesonbuild/meson/issues/825 # XXX: https://github.com/mesonbuild/meson/issues/825
docdir = get_option('datadir') / 'doc' / meson.project_name() docdir = get_option('datadir') / 'doc' / meson.project_name()
application_ns = 'name.janouch.' application_ns = 'name.janouch.'
application_url = 'https://janouch.name/p/' + meson.project_name()
conf = configuration_data() conf = configuration_data()
conf.set_quoted('PROJECT_NAME', meson.project_name()) conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@') conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
conf.set_quoted('PROJECT_NS', application_ns) conf.set_quoted('PROJECT_NS', application_ns)
conf.set_quoted('PROJECT_URL', application_url)
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir) conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
if win32 if win32
conf.set_quoted('PROJECT_DOCDIR', docdir) conf.set_quoted('PROJECT_DOCDIR', docdir)
@@ -138,7 +143,8 @@ if win32
'--width', size, '--height', size, '@INPUT@']) '--width', size, '--height', size, '@INPUT@'])
endforeach endforeach
icon_ico = custom_target(input : icon_png_list, output : 'fiv.ico', icon_ico = custom_target('fiv.ico',
output : 'fiv.ico', input : icon_png_list,
command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@']) command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
rc += windows.compile_resources('fiv.rc', depends : icon_ico) rc += windows.compile_resources('fiv.rc', depends : icon_ico)
endif endif
@@ -153,16 +159,21 @@ gresources = gnome.compile_resources('resources',
tiff_tables = custom_target('tiff-tables.h', tiff_tables = custom_target('tiff-tables.h',
output : 'tiff-tables.h', output : 'tiff-tables.h',
input : 'tiff-tables.db', input : 'tiff-tables.db',
command : ['tiff-tables.awk', '@INPUT@'], # Meson 0.56 chokes on files() as well as on a relative path.
command : [meson.current_source_dir() / 'tiff-tables.awk', '@INPUT@'],
capture : true, capture : true,
) )
desktops = ['fiv.desktop', 'fiv-browse.desktop'] desktops = ['fiv.desktop', 'fiv-browse.desktop']
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-io.c', 'fiv-context-menu.c', 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', 'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
'fiv-io-model.c', 'xdg.c', tiff_tables, gresources, rc, config, 'fiv-io-model.c', gresources, rc, config,
install : true, objects : iolib,
dependencies : dependencies, dependencies : dependencies,
install : true,
win_subsystem : 'windows', win_subsystem : 'windows',
) )
@@ -177,44 +188,55 @@ jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
) )
if get_option('tools').enabled() if get_option('tools').enabled()
# libjq 1.6 lacks a pkg-config file, and there is no release in sight. # libjq has only received a pkg-config file in version 1.7.
# libjq 1.6 is required. # libjq >= 1.6 is required.
tools_dependencies = [ tools_dependencies = [
cc.find_library('jq'), dependency('libpng'), dependency('libraw')] cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
tools_c_args = cc.get_supported_arguments( tools_c_args = cc.get_supported_arguments(
'-Wno-unused-function', '-Wno-unused-parameter') '-Wno-unused-function', '-Wno-unused-parameter')
foreach tool : ['info', 'pnginfo', 'rawinfo'] foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
executable(tool, 'tools/' + tool + '.c', tiff_tables, executable(tool, 'tools/' + tool + '.c', tiff_tables,
dependencies : tools_dependencies, dependencies : tools_dependencies,
c_args: tools_c_args) c_args: tools_c_args)
endforeach endforeach
if gdkpixbuf.found() if gdkpixbuf.found()
executable('benchmark-io', 'tools/benchmark-io.c', 'fiv-io.c', 'xdg.c', executable('benchmark-io', 'tools/benchmark-io.c',
tiff_tables, dependencies : [dependencies, gdkpixbuf]) objects : iolib,
dependencies : [dependencies, gdkpixbuf])
endif endif
endif endif
# Copying the files to the build directory makes GSettings find them in devenv.
gsettings_schemas = ['fiv.gschema.xml'] gsettings_schemas = ['fiv.gschema.xml']
foreach schema : gsettings_schemas foreach schema : gsettings_schemas
install_data(schema, configure_file(
rename : [application_ns + schema], input : schema,
output : application_ns + schema,
copy : true,
install: true,
install_dir : get_option('datadir') / 'glib-2.0' / 'schemas') install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
endforeach endforeach
# For the purposes of development: make the program find its GSettings schemas. # For the purposes of development: make the program find its GSettings schemas.
gnome.compile_schemas(depend_files : files(gsettings_schemas)) gnome.compile_schemas(depend_files : files(gsettings_schemas))
gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
# Validate various files, if there are tools around to do it. # Meson is broken on Windows and removes the backslashes, so this ends up empty.
xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \ symbolics = run_command(find_program('sed', required : false, disabler : true),
gsettings_schemas '-n', 's@.*>\\([^<>]*[.]svg\\)<.*@resources/\\1@p',
xmls += run_command(find_program('sed', required : false, disabler : true),
'-n', 's@.*>\([^<>]*[.]svg\)<.*@resources/\\1@p',
configure_file( configure_file(
input : 'resources/resources.gresource.xml', input : 'resources/resources.gresource.xml',
output : 'resources.gresource.xml.stamp', output : 'resources.gresource.xml.stamp',
copy : true, copy : true,
), capture : true, check : true).stdout().strip().split('\n') ), capture : true, check : true).stdout().strip()
# Validate various files, if there are tools around to do it.
xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
gsettings_schemas
if symbolics != ''
xmls += symbolics.split('\n')
endif
xmlwf = find_program('xmlwf', required : false, disabler : true) xmlwf = find_program('xmlwf', required : false, disabler : true)
xmllint = find_program('xmllint', required : false, disabler : true) xmllint = find_program('xmllint', required : false, disabler : true)
@@ -317,11 +339,44 @@ if not win32
if not meson.is_cross_build() if not meson.is_cross_build()
meson.add_install_script(updater, skip_if_destdir : dynamic_desktops) meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
endif endif
elif meson.is_cross_build()
msys2_root = meson.get_external_property('msys2_root')
meson.add_install_script('msys2-cross-install.sh', msys2_root)
# This is the minimum to run targets from msys2-cross-configure.sh builds. # 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({ meson.add_devenv({
'WINEPATH' : msys2_root / 'bin', 'WINEPATH' : msys2_root / 'bin',
'XDG_DATA_DIRS' : msys2_root / 'share', 'XDG_DATA_DIRS' : msys2_root / 'share',

View File

@@ -1,8 +1,26 @@
#!/bin/sh -e #!/bin/sh -e
# msys2-cross-configure.sh: set up an MSYS2-based cross-compiled Meson build. # msys2-configure.sh: set up an MSYS2-based Meson build (x86-64 by default)
# Dependencies: AWK, sed, sha256sum, cURL, bsdtar, #
# Dependencies: AWK, sed, coreutils, cURL, bsdtar (libarchive),
# wine64, Meson, mingw-w64-binutils, mingw-w64-gcc, pkg-config # wine64, Meson, mingw-w64-binutils, mingw-w64-gcc, pkg-config
repository=https://repo.msys2.org/mingw/mingw64/ #
# We support running directly from within MSYS2 on Windows,
# albeit while still downloading a complete copy of runtime depencies.
pkg=${MINGW_PACKAGE_PREFIX:-mingw-w64-x86_64}
prefix=${MSYSTEM_PREFIX:-/mingw64}
repo=https://repo.msys2.org/mingw$prefix
chost=${MSYSTEM_CHOST:-x86_64-w64-mingw32}
carch=${MSYSTEM_CARCH:-x86_64}
[ "$carch" = "i686" ] && carch=x86
if [ -n "$MSYSTEM" ]
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-msitools $pkg-pkgconf
fi
status() { status() {
echo "$(tput bold)-- $*$(tput sgr0)" echo "$(tput bold)-- $*$(tput sgr0)"
@@ -10,7 +28,7 @@ status() {
dbsync() { dbsync() {
status Fetching repository DB status Fetching repository DB
[ -f db.tsv ] || curl -# "$repository/mingw64.db" | bsdtar -xOf- | awk ' [ -f db.tsv ] || curl -# "$repo$prefix.db" | bsdtar -xOf- | awk '
function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] } function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] }
NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] } NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" } !/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
@@ -28,10 +46,11 @@ fetch() {
} BEGIN { while ((getline < "db.tsv") > 0) { } BEGIN { while ((getline < "db.tsv") > 0) {
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) { filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS } 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 do
status Fetching "$name" status Fetching "$name"
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repository/$name" [ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
done done
version=$(curl -# https://exiftool.org/ver.txt) version=$(curl -# https://exiftool.org/ver.txt)
@@ -51,14 +70,20 @@ extract() {
for subdir in * for subdir in *
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir" do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
done done
for i in packages/* while IFS= read -r name
do bsdtar -xf "$i" --strip-components 1 mingw64 do bsdtar -xf "packages/$name" --strip-components 1 \
done --exclude '*/share/man' --exclude '*/share/doc'
done < db.want
# Don't require Perl, which may not exist anymore on i686:
# https://github.com/msys2/MINGW-packages/pull/20085
if [ -d lib/perl5 ]
then
bsdtar -xf exiftool.tar.gz bsdtar -xf exiftool.tar.gz
mv Image-ExifTool-*/exiftool bin mv Image-ExifTool-*/exiftool bin
mv Image-ExifTool-*/lib/* lib/perl5/site_perl mv Image-ExifTool-*/lib/* lib/perl5/site_perl
rm -rf Image-ExifTool-* rm -rf Image-ExifTool-*
fi
} }
configure() { configure() {
@@ -74,49 +99,50 @@ configure() {
setup() { setup() {
status Setting up Meson status Setting up Meson
wrap=true pclibdir=$msys2_root/share/pkgconfig:$msys2_root/lib/pkgconfig
[ -n "$MSYSTEM" ] && \
wrap=false pclibdir="$(pwd -W)/share/pkgconfig;$(pwd -W)/lib/pkgconfig"
cat >"$toolchain" <<-EOF cat >"$toolchain" <<-EOF
[binaries] [binaries]
c = 'x86_64-w64-mingw32-gcc' c = '$chost-gcc'
cpp = 'x86_64-w64-mingw32-g++' cpp = '$chost-g++'
ar = 'x86_64-w64-mingw32-gcc-ar' ar = '$chost-gcc-ar'
ranlib = 'x86_64-w64-mingw32-gcc-ranlib' ranlib = '$chost-gcc-ranlib'
strip = 'x86_64-w64-mingw32-strip' strip = '$chost-strip'
windres = 'x86_64-w64-mingw32-windres' windres = '$chost-windres'
pkgconfig = 'pkg-config' pkgconfig = 'pkg-config'
[properties] [properties]
sys_root = '$builddir' sys_root = '$builddir'
msys2_root = '$msys2_root' msys2_root = '$msys2_root'
pkg_config_libdir = '$msys2_root/share/pkgconfig:$msys2_root/lib/pkgconfig' pkg_config_libdir = '$pclibdir'
needs_exe_wrapper = true needs_exe_wrapper = $wrap
[host_machine] [host_machine]
system = 'windows' system = 'windows'
cpu_family = 'x86_64' cpu_family = '$carch'
cpu = 'x86_64' cpu = '$carch'
endian = 'little' endian = 'little'
EOF EOF
meson setup --buildtype=debugoptimized --prefix="$packagedir" \ meson setup --buildtype=debugoptimized --prefix=/ \
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir" --bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
} }
sourcedir=$(realpath "${2:-$(dirname "$0")}") sourcedir=$(realpath "${2:-$(dirname "$0")}")
builddir=$(realpath "${1:-builddir}") builddir=$(realpath "${1:-builddir}")
packagedir=$builddir/package
toolchain=$builddir/msys2-cross-toolchain.meson toolchain=$builddir/msys2-cross-toolchain.meson
# This directory name matches the prefix in .pc files, so we don't need to # This directory name matches the prefix in .pc files, so we don't need to
# modify them (pkgconf has --prefix-variable, but Meson can't pass that option). # modify them (pkgconf has --prefix-variable, but Meson can't pass that option).
msys2_root=$builddir/mingw64 msys2_root=$builddir$prefix
mkdir -p "$msys2_root" mkdir -p "$msys2_root"
cd "$msys2_root" cd "$msys2_root"
dbsync dbsync
fetch mingw-w64-x86_64-gtk3 mingw-w64-x86_64-lcms2 \ fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-libjxl $pkg-perl \
mingw-w64-x86_64-libraw mingw-w64-x86_64-libheif \ $pkg-perl-win32-api $pkg-libwinpthread-git # Because we don't do "provides"?
mingw-w64-x86_64-perl mingw-w64-x86_64-perl-win32-api \
mingw-w64-x86_64-libwinpthread-git # Because we don't do "provides"?
verify verify
extract extract
configure configure

View File

@@ -3,25 +3,33 @@ export LC_ALL=C
cd "$MESON_INSTALL_DESTDIR_PREFIX" cd "$MESON_INSTALL_DESTDIR_PREFIX"
msys2_root=$1 msys2_root=$1
# Support running directly from within MSYS2 on Windows.
if [ -n "$MSYSTEM" ]
then
wine64() { "$@"; }
awk() { command awk -v RS='\r?\n' "$@"; }
fi
# Copy binaries we directly or indirectly depend on. # Copy binaries we directly or indirectly depend on.
cp -p "$msys2_root"/bin/*.dll . cp -p "$msys2_root"/bin/*.dll .
cp -p "$msys2_root"/bin/wperl.exe . cp -p "$msys2_root"/bin/wperl.exe . || :
cp -p "$msys2_root"/bin/exiftool . cp -p "$msys2_root"/bin/exiftool . || :
# The console helper is only useful for debug builds. # The console helper is only useful for debug builds.
cp -p "$msys2_root"/bin/gspawn-*-helper*.exe . cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
cp -pR "$msys2_root"/etc/ . cp -pR "$msys2_root"/etc/ .
mkdir -p lib mkdir -p lib
cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib
cp -pR "$msys2_root"/lib/perl5/ lib cp -pR "$msys2_root"/lib/perl5/ lib || :
mkdir -p share/glib-2.0/schemas mkdir -p share/glib-2.0/schemas
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
mkdir -p share
cp -pR "$msys2_root"/share/mime/ share
mkdir -p share/icons mkdir -p share/icons
cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons
mkdir -p share/icons/hicolor mkdir -p share/icons/hicolor
cp -p "$msys2_root"/share/icons/hicolor/index.theme 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. # Remove unreferenced libraries.
find lib -name '*.a' -exec rm -- {} + find lib -name '*.a' -exec rm -- {} +

35
msys2-package.sh Executable file
View File

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

View File

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

View File

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

View File

@@ -156,12 +156,18 @@ tiffer_next_ifd(struct tiffer *self)
return tiffer_u16(self, &self->remaining_fields); return tiffer_u16(self, &self->remaining_fields);
} }
static size_t
tiffer_length(const struct tiffer *self)
{
return self->begin > self->end ? 0 : self->end - self->begin;
}
/// Initialize a derived TIFF reader for a subIFD at the given location. /// Initialize a derived TIFF reader for a subIFD at the given location.
static bool static bool
tiffer_subifd( tiffer_subifd(
const struct tiffer *self, uint32_t offset, struct tiffer *subreader) const struct tiffer *self, uint32_t offset, struct tiffer *subreader)
{ {
if (self->end - self->begin < offset) if (tiffer_length(self) < offset)
return false; return false;
*subreader = *self; *subreader = *self;
@@ -332,7 +338,7 @@ tiffer_next_entry(struct tiffer *self, struct tiffer_entry *entry)
if (values_size <= sizeof offset) { if (values_size <= sizeof offset) {
entry->p = self->p; entry->p = self->p;
self->p += sizeof offset; self->p += sizeof offset;
} else if (tiffer_u32(self, &offset) && self->end - self->begin >= offset) { } else if (tiffer_u32(self, &offset) && tiffer_length(self) >= offset) {
entry->p = self->begin + offset; entry->p = self->begin + offset;
} else { } else {
return false; return false;

View File

@@ -41,14 +41,14 @@ one_file(const char *filename)
.warnings = g_ptr_array_new_with_free_func(g_free), .warnings = g_ptr_array_new_with_free_func(g_free),
}; };
cairo_surface_t *loaded_by_us = fiv_io_open(&ctx, NULL); FivIoImage *loaded_by_us = fiv_io_open(&ctx, NULL);
g_clear_object(&file); g_clear_object(&file);
g_free((char *) ctx.uri); g_free((char *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE); g_ptr_array_free(ctx.warnings, TRUE);
if (!loaded_by_us) if (!loaded_by_us)
return; return;
cairo_surface_destroy(loaded_by_us); fiv_io_image_unref(loaded_by_us);
us = timestamp() - since_us; us = timestamp() - since_us;
double since_pixbuf = timestamp(), pixbuf = 0; double since_pixbuf = timestamp(), pixbuf = 0;

210
tools/hotpixels.c Normal file
View File

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

View File

@@ -123,6 +123,39 @@ parse_exif_subifds(const struct tiffer *T, struct tiffer_entry *entry,
return a; 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 static jv
parse_exif_ascii(struct tiffer_entry *entry) 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); v = parse_exif_subifds(T, entry, subentries);
} else if (entry->type == TIFFER_ASCII) { } else if (entry->type == TIFFER_ASCII) {
v = parse_exif_extract_sole_array_element(parse_exif_ascii(entry)); 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) { } else if (entry->type == TIFFER_UNDEFINED && !info->values) {
// Several Exif entries of UNDEFINED type contain single-byte numbers. // Several Exif entries of UNDEFINED type contain single-byte numbers.
v = parse_exif_undefined(entry); v = parse_exif_undefined(entry);

3
xdg.c
View File

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