Compare commits
	
		
			83 Commits
		
	
	
		
			2e8bbf0e43
			...
			v1.0.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						3bea18708f
	
				 | 
					
					
						|||
| 
						
						
							
						
						ed8ba147ba
	
				 | 
					
					
						|||
| 
						
						
							
						
						c221a00c33
	
				 | 
					
					
						|||
| 
						
						
							
						
						192ffa0de9
	
				 | 
					
					
						|||
| 
						
						
							
						
						bac9fce4e0
	
				 | 
					
					
						|||
| 
						
						
							
						
						2e9ea9b4e2
	
				 | 
					
					
						|||
| 
						
						
							
						
						b34fe63198
	
				 | 
					
					
						|||
| 
						
						
							
						
						3c8ddcaf26
	
				 | 
					
					
						|||
| 
						
						
							
						
						e3ec07a19f
	
				 | 
					
					
						|||
| 
						
						
							
						
						e57364cd97
	
				 | 
					
					
						|||
| 
						
						
							
						
						7330f07dd7
	
				 | 
					
					
						|||
| 
						
						
							
						
						d68e09525c
	
				 | 
					
					
						|||
| 
						
						
							
						
						115a7bab0f
	
				 | 
					
					
						|||
| 
						
						
							
						
						91538aaba5
	
				 | 
					
					
						|||
| 
						
						
							
						
						c214e668d9
	
				 | 
					
					
						|||
| 
						
						
							
						
						a5ebc697ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ca18f52d5
	
				 | 
					
					
						|||
| 
						
						
							
						
						604594a8f1
	
				 | 
					
					
						|||
| 
						
						
							
						
						9acab00bcc
	
				 | 
					
					
						|||
| 
						
						
							
						
						ae8dc3070a
	
				 | 
					
					
						|||
| 
						
						
							
						
						3c8a280546
	
				 | 
					
					
						|||
| 
						
						
							
						
						96189b70b8
	
				 | 
					
					
						|||
| 
						
						
							
						
						67433f3776
	
				 | 
					
					
						|||
| 
						
						
							
						
						c1418c7462
	
				 | 
					
					
						|||
| 
						
						
							
						
						935506b120
	
				 | 
					
					
						|||
| 
						
						
							
						
						84269b2ba2
	
				 | 
					
					
						|||
| 
						
						
							
						
						51ca3f8e2e
	
				 | 
					
					
						|||
| 
						
						
							
						
						f196b03e97
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee08565389
	
				 | 
					
					
						|||
| 
						
						
							
						
						c04c4063e4
	
				 | 
					
					
						|||
| 
						
						
							
						
						aed6ae6b83
	
				 | 
					
					
						|||
| 
						
						
							
						
						bae640a116
	
				 | 
					
					
						|||
| 
						
						
							
						
						52c17c8a16
	
				 | 
					
					
						|||
| 
						
						
							
						
						b07fba0c9c
	
				 | 
					
					
						|||
| 
						
						
							
						
						72bf913f3d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e79574fd56
	
				 | 
					
					
						|||
| 
						
						
							
						
						93ad75eb35
	
				 | 
					
					
						|||
| 
						
						
							
						
						2d10aa8b61
	
				 | 
					
					
						|||
| 
						
						
							
						
						1ec41f7749
	
				 | 
					
					
						|||
| 
						
						
							
						
						d4b91d6260
	
				 | 
					
					
						|||
| 
						
						
							
						
						5ec5f5bdbd
	
				 | 
					
					
						|||
| 
						
						
							
						
						840e7f172c
	
				 | 
					
					
						|||
| 
						
						
							
						
						9b99de99bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						ab75d2b61d
	
				 | 
					
					
						|||
| 
						
						
							
						
						92deba3890
	
				 | 
					
					
						|||
| 
						
						
							
						
						668c5eb78a
	
				 | 
					
					
						|||
| 
						
						
							
						
						d713d5820c
	
				 | 
					
					
						|||
| 
						
						
							
						
						f05e66bfc1
	
				 | 
					
					
						|||
| 
						
						
							
						
						6ee5f69bfe
	
				 | 
					
					
						|||
| 
						
						
							
						
						4249898497
	
				 | 
					
					
						|||
| 
						
						
							
						
						117422ade5
	
				 | 
					
					
						|||
| 
						
						
							
						
						8ff33e6b63
	
				 | 
					
					
						|||
| 
						
						
							
						
						ce4a13ed38
	
				 | 
					
					
						|||
| 
						
						
							
						
						6a1b851130
	
				 | 
					
					
						|||
| 
						
						
							
						
						68245b55c9
	
				 | 
					
					
						|||
| 
						
						
							
						
						2869c656c1
	
				 | 
					
					
						|||
| 
						
						
							
						
						ec713b633e
	
				 | 
					
					
						|||
| 
						
						
							
						
						88234f8283
	
				 | 
					
					
						|||
| 
						
						
							
						
						49ee551b9b
	
				 | 
					
					
						|||
| 
						
						
							
						
						089c90004b
	
				 | 
					
					
						|||
| 
						
						
							
						
						19913a5e48
	
				 | 
					
					
						|||
| 
						
						
							
						
						1ef0a84bc7
	
				 | 
					
					
						|||
| 
						
						
							
						
						4b5b8ec9fa
	
				 | 
					
					
						|||
| 
						
						
							
						
						3449ac5a12
	
				 | 
					
					
						|||
| 
						
						
							
						
						bbfa2344d6
	
				 | 
					
					
						|||
| 
						
						
							
						
						2ff853b7e0
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb4d3acd12
	
				 | 
					
					
						|||
| 
						
						
							
						
						074bd4d37f
	
				 | 
					
					
						|||
| 
						
						
							
						
						add96b37a6
	
				 | 
					
					
						|||
| 
						
						
							
						
						c2e8b65d0f
	
				 | 
					
					
						|||
| 
						
						
							
						
						4f57070e27
	
				 | 
					
					
						|||
| 
						
						
							
						
						2dc4e9c13b
	
				 | 
					
					
						|||
| 
						
						
							
						
						a1f6ffd226
	
				 | 
					
					
						|||
| 
						
						
							
						
						1eee1831a5
	
				 | 
					
					
						|||
| 
						
						
							
						
						86622e0c31
	
				 | 
					
					
						|||
| 
						
						
							
						
						a4772ce319
	
				 | 
					
					
						|||
| 
						
						
							
						
						0318424540
	
				 | 
					
					
						|||
| 
						
						
							
						
						8d5885bfdf
	
				 | 
					
					
						|||
| 
						
						
							
						
						41b5ddc744
	
				 | 
					
					
						|||
| 
						
						
							
						
						b308b5da18
	
				 | 
					
					
						|||
| 
						
						
							
						
						1577961aa2
	
				 | 
					
					
						|||
| 
						
						
							
						
						1fb42e689f
	
				 | 
					
					
						|||
| 
						
						
							
						
						8953e6beea
	
				 | 
					
					
						
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
 | 
			
		||||
Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
purpose with or without fee is hereby granted.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								README.adoc
									
									
									
									
									
								
							@@ -2,7 +2,7 @@ fiv
 | 
			
		||||
===
 | 
			
		||||
 | 
			
		||||
'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"]
 | 
			
		||||
 | 
			
		||||
@@ -13,7 +13,7 @@ Features
 | 
			
		||||
   photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
 | 
			
		||||
   modules manage to load.
 | 
			
		||||
 - Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
 | 
			
		||||
 - Makes use of 30-bit X.org visuals, whenever it's possible and appropriate.
 | 
			
		||||
 - Can make use of 30-bit X.org visuals, under certain conditions.
 | 
			
		||||
 - Has a notion of pages, and tries to load all included content within files.
 | 
			
		||||
 - Can keep the zoom and position when browsing, to help with comparing
 | 
			
		||||
   zoomed-in images.
 | 
			
		||||
@@ -33,15 +33,20 @@ Not necessarily in this order.
 | 
			
		||||
 | 
			
		||||
Packages
 | 
			
		||||
--------
 | 
			
		||||
Regular releases are sporadic.  git master should be stable enough.  You can get
 | 
			
		||||
a package with the latest development version from Archlinux's AUR.
 | 
			
		||||
Regular releases are sporadic.  git master should be stable enough.
 | 
			
		||||
You can get a package with the latest development version using Arch Linux's
 | 
			
		||||
https://aur.archlinux.org/packages/fiv-git[AUR],
 | 
			
		||||
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
 | 
			
		||||
 | 
			
		||||
https://janouch.name/cd[Windows installers can be found here],
 | 
			
		||||
you want the _x86_64_ version.
 | 
			
		||||
 | 
			
		||||
Building and Running
 | 
			
		||||
--------------------
 | 
			
		||||
Build-only dependencies:
 | 
			
		||||
 Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
 | 
			
		||||
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
 | 
			
		||||
 libturbojpeg, libwebp, librsvg-2.0 (for icons) +
 | 
			
		||||
 libturbojpeg, libwebp, libepoxy, librsvg-2.0 (for icons) +
 | 
			
		||||
Optional dependencies: lcms2, Little CMS fast float plugin,
 | 
			
		||||
 LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
 | 
			
		||||
 resvg (unstable API, needs to be requested explicitly) +
 | 
			
		||||
@@ -49,30 +54,42 @@ Runtime dependencies for reverse image search:
 | 
			
		||||
 xdg-utils, cURL, jq
 | 
			
		||||
 | 
			
		||||
 $ git clone --recursive https://git.janouch.name/p/fiv.git
 | 
			
		||||
 $ cd fiv
 | 
			
		||||
 $ meson setup builddir
 | 
			
		||||
 $ cd builddir
 | 
			
		||||
 $ meson compile
 | 
			
		||||
 | 
			
		||||
Considering the vast amount of dynamically-linked dependencies, do not attempt
 | 
			
		||||
direct installations via `ninja install`.  To test the program:
 | 
			
		||||
 | 
			
		||||
 $ meson devenv fiv
 | 
			
		||||
 | 
			
		||||
The lossless JPEG cropper and reverse image search are intended to be invoked
 | 
			
		||||
from a context menu.
 | 
			
		||||
from a file manager context menu.
 | 
			
		||||
 | 
			
		||||
For proper integration, you will need to install the application.  On Debian,
 | 
			
		||||
you can get a quick and dirty installation package for testing purposes using:
 | 
			
		||||
 | 
			
		||||
 $ meson compile deb
 | 
			
		||||
 # dpkg -i fiv-*.deb
 | 
			
		||||
 | 
			
		||||
Windows
 | 
			
		||||
~~~~~~~
 | 
			
		||||
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
 | 
			
		||||
dependencies listed at the beginning of 'msys2-cross-configure.sh',
 | 
			
		||||
plus rsvg-convert from librsvg2, and icotool from icoutils.
 | 
			
		||||
dependencies listed at the beginning of 'msys2-configure.sh',
 | 
			
		||||
plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
 | 
			
		||||
Beware that the build will take up about a gigabyte of disk space.
 | 
			
		||||
 | 
			
		||||
 $ sh -e msys2-cross-configure.sh builddir
 | 
			
		||||
 $ meson install -C builddir
 | 
			
		||||
 $ sh -e msys2-configure.sh builddir
 | 
			
		||||
 $ meson compile package -C builddir
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
-------------
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,10 @@ Options
 | 
			
		||||
	handler to implement the "Open Containing Folder" feature of certain
 | 
			
		||||
	applications.
 | 
			
		||||
 | 
			
		||||
*--collection*::
 | 
			
		||||
	Always put arguments in a virtual directory, even when only one is passed.
 | 
			
		||||
	Implies *--browse*.
 | 
			
		||||
 | 
			
		||||
*--help-all*::
 | 
			
		||||
	Show the full list of options, including those provided by GTK+.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ q:lang(en):after { content: "’"; }
 | 
			
		||||
<p class="details">
 | 
			
		||||
<span id="author">Přemysl Eric Janouch</span><br>
 | 
			
		||||
<span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br>
 | 
			
		||||
<span id="revnumber">version 0.0.0,</span>
 | 
			
		||||
<span id="revnumber">version 1.0.0,</span>
 | 
			
		||||
<span id="revdate">2023-04-17</span>
 | 
			
		||||
 | 
			
		||||
<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
 | 
			
		||||
@@ -95,14 +95,8 @@ rm -rf ~/.cache/thumbnails/wide-*
 | 
			
		||||
 | 
			
		||||
<h2>Configuration</h2>
 | 
			
		||||
 | 
			
		||||
<p>The few configuration options <i>fiv</i> has can be adjusted using
 | 
			
		||||
<i>dconf-editor</i>, which can be launched in the appropriate location from
 | 
			
		||||
within the application by pressing <kbd>Ctrl</kbd> + <kbd>,</kbd>.
 | 
			
		||||
For command line usage, there is the <i>gsettings</i> utility:
 | 
			
		||||
 | 
			
		||||
<pre>
 | 
			
		||||
gsettings list-recursively name.janouch.fiv
 | 
			
		||||
</pre>
 | 
			
		||||
<p>To adjust the few configuration options of <i>fiv</i>,
 | 
			
		||||
press <kbd>Ctrl</kbd> + <kbd>,</kbd> to open <i>Preferences</i>.
 | 
			
		||||
 | 
			
		||||
<p>To make your changes take effect, restart <i>fiv</i>.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docs/fiv.webp
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/fiv.webp
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 194 KiB  | 
							
								
								
									
										114
									
								
								fiv-browser.c
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								fiv-browser.c
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
//
 | 
			
		||||
// fiv-browser.c: filesystem browsing widget
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -17,9 +17,6 @@
 | 
			
		||||
 | 
			
		||||
#include "config.h"
 | 
			
		||||
 | 
			
		||||
#include <math.h>
 | 
			
		||||
#include <pixman.h>
 | 
			
		||||
 | 
			
		||||
#include <gtk/gtk.h>
 | 
			
		||||
#ifdef GDK_WINDOWING_X11
 | 
			
		||||
#include <gdk/gdkx.h>
 | 
			
		||||
@@ -27,6 +24,10 @@
 | 
			
		||||
#ifdef GDK_WINDOWING_QUARTZ
 | 
			
		||||
#include <gdk/gdkquartz.h>
 | 
			
		||||
#endif  // GDK_WINDOWING_QUARTZ
 | 
			
		||||
#include <pixman.h>
 | 
			
		||||
 | 
			
		||||
#include <math.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
 | 
			
		||||
#include "fiv-browser.h"
 | 
			
		||||
#include "fiv-collection.h"
 | 
			
		||||
@@ -91,7 +92,8 @@ struct _FivBrowser {
 | 
			
		||||
 | 
			
		||||
	Thumbnailer *thumbnailers;          ///< Parallelized thumbnailers
 | 
			
		||||
	size_t thumbnailers_len;            ///< Thumbnailers array size
 | 
			
		||||
	GQueue thumbnailers_queue;          ///< Queued up Entry pointers
 | 
			
		||||
	GQueue thumbnailers_queue_1;        ///< Queued up Entry pointers, hi-prio
 | 
			
		||||
	GQueue thumbnailers_queue_2;        ///< Queued up Entry pointers, lo-prio
 | 
			
		||||
 | 
			
		||||
	GdkCursor *pointer;                 ///< Cached pointer cursor
 | 
			
		||||
	cairo_pattern_t *glow;              ///< CAIRO_FORMAT_A8 mask for corners
 | 
			
		||||
@@ -240,10 +242,12 @@ relayout(FivBrowser *self, int width)
 | 
			
		||||
			pango_layout_set_wrap(label, PANGO_WRAP_WORD_CHAR);
 | 
			
		||||
			pango_layout_set_ellipsize(label, PANGO_ELLIPSIZE_END);
 | 
			
		||||
 | 
			
		||||
#if PANGO_VERSION_CHECK(1, 44, 0)
 | 
			
		||||
			PangoAttrList *attrs = pango_attr_list_new();
 | 
			
		||||
			pango_attr_list_insert(attrs, pango_attr_insert_hyphens_new(FALSE));
 | 
			
		||||
			pango_layout_set_attributes(label, attrs);
 | 
			
		||||
			pango_attr_list_unref (attrs);
 | 
			
		||||
#endif
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		g_array_append_val(items, ((Item) {
 | 
			
		||||
@@ -272,14 +276,13 @@ relayout(FivBrowser *self, int width)
 | 
			
		||||
		gtk_adjustment_set_page_size(self->hadjustment, width);
 | 
			
		||||
	}
 | 
			
		||||
	if (self->vadjustment) {
 | 
			
		||||
		int height = gtk_widget_get_allocated_height(widget);
 | 
			
		||||
		gtk_adjustment_set_lower(self->vadjustment, 0);
 | 
			
		||||
		gtk_adjustment_set_upper(self->vadjustment, total_height);
 | 
			
		||||
		gtk_adjustment_set_upper(self->vadjustment, MAX(height, total_height));
 | 
			
		||||
		gtk_adjustment_set_step_increment(self->vadjustment,
 | 
			
		||||
			self->item_height + self->item_spacing + 2 * self->item_border_y);
 | 
			
		||||
		gtk_adjustment_set_page_increment(
 | 
			
		||||
			self->vadjustment, gtk_widget_get_allocated_height(widget) * 0.9);
 | 
			
		||||
		gtk_adjustment_set_page_size(
 | 
			
		||||
			self->vadjustment, gtk_widget_get_allocated_height(widget));
 | 
			
		||||
		gtk_adjustment_set_page_increment(self->vadjustment, height * 0.9);
 | 
			
		||||
		gtk_adjustment_set_page_size(self->vadjustment, height);
 | 
			
		||||
	}
 | 
			
		||||
	return total_height;
 | 
			
		||||
}
 | 
			
		||||
@@ -740,7 +743,7 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
 | 
			
		||||
	if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
 | 
			
		||||
		cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
 | 
			
		||||
			(void *) (intptr_t) 1, NULL);
 | 
			
		||||
		g_queue_push_tail(&self->thumbnailers_queue, entry);
 | 
			
		||||
		g_queue_push_tail(&self->thumbnailers_queue_2, entry);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	entry_set_surface_user_data(entry);
 | 
			
		||||
@@ -794,13 +797,21 @@ on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
 | 
			
		||||
	thumbnailer_next(t);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO(p): Try to keep the minions alive (stdout will be a problem).
 | 
			
		||||
static gboolean
 | 
			
		||||
thumbnailer_next(Thumbnailer *t)
 | 
			
		||||
{
 | 
			
		||||
	// TODO(p): Try to keep the minions alive (stdout will be a problem).
 | 
			
		||||
	// Already have something to do, not a failure.
 | 
			
		||||
	if (t->target)
 | 
			
		||||
		return TRUE;
 | 
			
		||||
 | 
			
		||||
	// They could have been removed via post-reload changes in the model.
 | 
			
		||||
	FivBrowser *self = t->self;
 | 
			
		||||
	if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue)))
 | 
			
		||||
		return FALSE;
 | 
			
		||||
	do {
 | 
			
		||||
		if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue_1)) &&
 | 
			
		||||
			!(t->target = g_queue_pop_head(&self->thumbnailers_queue_2)))
 | 
			
		||||
			return FALSE;
 | 
			
		||||
	} while (t->target->removed);
 | 
			
		||||
 | 
			
		||||
	// Case analysis:
 | 
			
		||||
	//  - We haven't found any thumbnail for the entry at all
 | 
			
		||||
@@ -817,9 +828,18 @@ thumbnailer_next(Thumbnailer *t)
 | 
			
		||||
		"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
 | 
			
		||||
		"--", uri, NULL};
 | 
			
		||||
 | 
			
		||||
	GSubprocessLauncher *launcher =
 | 
			
		||||
		g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE);
 | 
			
		||||
#ifdef G_OS_WIN32
 | 
			
		||||
	gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
 | 
			
		||||
	g_subprocess_launcher_set_cwd(launcher, prefix);
 | 
			
		||||
	g_free(prefix);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower,
 | 
			
		||||
		G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
 | 
			
		||||
	t->minion = g_subprocess_launcher_spawnv(
 | 
			
		||||
		launcher, t->target->icon ? argv_faster : argv_slower, &error);
 | 
			
		||||
	g_object_unref(launcher);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		g_warning("%s", error->message);
 | 
			
		||||
		g_error_free(error);
 | 
			
		||||
@@ -837,7 +857,8 @@ thumbnailer_next(Thumbnailer *t)
 | 
			
		||||
static void
 | 
			
		||||
thumbnailers_abort(FivBrowser *self)
 | 
			
		||||
{
 | 
			
		||||
	g_queue_clear(&self->thumbnailers_queue);
 | 
			
		||||
	g_queue_clear(&self->thumbnailers_queue_1);
 | 
			
		||||
	g_queue_clear(&self->thumbnailers_queue_2);
 | 
			
		||||
 | 
			
		||||
	for (size_t i = 0; i < self->thumbnailers_len; i++) {
 | 
			
		||||
		Thumbnailer *t = self->thumbnailers + i;
 | 
			
		||||
@@ -853,35 +874,35 @@ thumbnailers_abort(FivBrowser *self)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
thumbnailers_start(FivBrowser *self)
 | 
			
		||||
thumbnailers_enqueue(FivBrowser *self, Entry *entry)
 | 
			
		||||
{
 | 
			
		||||
	thumbnailers_abort(self);
 | 
			
		||||
	if (!self->model)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	GQueue lq = G_QUEUE_INIT;
 | 
			
		||||
	for (guint i = 0; i < self->entries->len; i++) {
 | 
			
		||||
		Entry *entry = self->entries->pdata[i];
 | 
			
		||||
		if (entry->removed)
 | 
			
		||||
			continue;
 | 
			
		||||
 | 
			
		||||
	if (!entry->removed) {
 | 
			
		||||
		if (entry->icon)
 | 
			
		||||
			g_queue_push_tail(&self->thumbnailers_queue, entry);
 | 
			
		||||
			g_queue_push_tail(&self->thumbnailers_queue_1, entry);
 | 
			
		||||
		else if (cairo_surface_get_user_data(
 | 
			
		||||
			entry->thumbnail, &fiv_thumbnail_key_lq))
 | 
			
		||||
			g_queue_push_tail(&lq, entry);
 | 
			
		||||
	}
 | 
			
		||||
	while (!g_queue_is_empty(&lq)) {
 | 
			
		||||
		g_queue_push_tail_link(
 | 
			
		||||
			&self->thumbnailers_queue, g_queue_pop_head_link(&lq));
 | 
			
		||||
			g_queue_push_tail(&self->thumbnailers_queue_2, entry);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
thumbnailers_deploy(FivBrowser *self)
 | 
			
		||||
{
 | 
			
		||||
	for (size_t i = 0; i < self->thumbnailers_len; i++) {
 | 
			
		||||
		if (!thumbnailer_next(self->thumbnailers + i))
 | 
			
		||||
			break;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
thumbnailers_restart(FivBrowser *self)
 | 
			
		||||
{
 | 
			
		||||
	thumbnailers_abort(self);
 | 
			
		||||
	for (guint i = 0; i < self->entries->len; i++)
 | 
			
		||||
		thumbnailers_enqueue(self, self->entries->pdata[i]);
 | 
			
		||||
	thumbnailers_deploy(self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Boilerplate -------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
 | 
			
		||||
@@ -1002,7 +1023,7 @@ set_item_size(FivBrowser *self, FivThumbnailSize size)
 | 
			
		||||
 | 
			
		||||
		g_hash_table_remove_all(self->thumbnail_cache);
 | 
			
		||||
		reload_thumbnails(self);
 | 
			
		||||
		thumbnailers_start(self);
 | 
			
		||||
		thumbnailers_restart(self);
 | 
			
		||||
 | 
			
		||||
		g_object_notify_by_pspec(
 | 
			
		||||
			G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
 | 
			
		||||
@@ -1560,6 +1581,14 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
 | 
			
		||||
	switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
 | 
			
		||||
	case 0:
 | 
			
		||||
		switch (event->keyval) {
 | 
			
		||||
		case GDK_KEY_Delete:
 | 
			
		||||
			if (self->selected) {
 | 
			
		||||
				GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
 | 
			
		||||
				GFile *file = g_file_new_for_uri(self->selected->e->uri);
 | 
			
		||||
				fiv_context_menu_remove(window, file);
 | 
			
		||||
				g_object_unref(file);
 | 
			
		||||
			}
 | 
			
		||||
			return GDK_EVENT_STOP;
 | 
			
		||||
		case GDK_KEY_Return:
 | 
			
		||||
			if (self->selected)
 | 
			
		||||
				return open_entry(widget, self->selected, FALSE);
 | 
			
		||||
@@ -1862,7 +1891,8 @@ fiv_browser_init(FivBrowser *self)
 | 
			
		||||
		g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
 | 
			
		||||
	for (size_t i = 0; i < self->thumbnailers_len; i++)
 | 
			
		||||
		self->thumbnailers[i].self = self;
 | 
			
		||||
	g_queue_init(&self->thumbnailers_queue);
 | 
			
		||||
	g_queue_init(&self->thumbnailers_queue_1);
 | 
			
		||||
	g_queue_init(&self->thumbnailers_queue_2);
 | 
			
		||||
 | 
			
		||||
	set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL);
 | 
			
		||||
	self->show_labels = FALSE;
 | 
			
		||||
@@ -1907,8 +1937,9 @@ on_model_reloaded(FivIoModel *model, FivBrowser *self)
 | 
			
		||||
	fiv_browser_select(self, selected_uri);
 | 
			
		||||
	g_free(selected_uri);
 | 
			
		||||
 | 
			
		||||
	// Restarting thumbnailers is critical, because they keep Entry pointers.
 | 
			
		||||
	reload_thumbnails(self);
 | 
			
		||||
	thumbnailers_start(self);
 | 
			
		||||
	thumbnailers_restart(self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
@@ -1923,8 +1954,8 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
 | 
			
		||||
		g_ptr_array_add(self->entries, entry);
 | 
			
		||||
 | 
			
		||||
		reload_one_thumbnail(self, entry);
 | 
			
		||||
		// TODO(p): Try to add to thumbnailer queue if already started.
 | 
			
		||||
		thumbnailers_start(self);
 | 
			
		||||
		thumbnailers_enqueue(self, entry);
 | 
			
		||||
		thumbnailers_deploy(self);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -1950,8 +1981,9 @@ on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
 | 
			
		||||
		// so that there's no jumping around. Or, a bit more properly,
 | 
			
		||||
		// move the thumbnail cache entry to the new URI.
 | 
			
		||||
		reload_one_thumbnail(self, found);
 | 
			
		||||
		// TODO(p): Try to add to thumbnailer queue if already started.
 | 
			
		||||
		thumbnailers_start(self);
 | 
			
		||||
		// TODO(p): Rather cancel the entry in any running thumbnailer,
 | 
			
		||||
		// remove it from queues, and _enqueue() + _deploy().
 | 
			
		||||
		thumbnailers_restart(self);
 | 
			
		||||
	} else {
 | 
			
		||||
		found->removed = TRUE;
 | 
			
		||||
		gtk_widget_queue_draw(GTK_WIDGET(self));
 | 
			
		||||
 
 | 
			
		||||
@@ -528,12 +528,16 @@ fiv_collection_file_query_info(GFile *file, const char *attributes,
 | 
			
		||||
		g_file_info_set_name(info, basename);
 | 
			
		||||
		g_free(basename);
 | 
			
		||||
 | 
			
		||||
		if ((name = g_file_info_get_display_name(info))) {
 | 
			
		||||
		if (g_file_info_has_attribute(
 | 
			
		||||
				info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) &&
 | 
			
		||||
			(name = g_file_info_get_display_name(info))) {
 | 
			
		||||
			gchar *prefixed = get_prefixed_name(self, name);
 | 
			
		||||
			g_file_info_set_display_name(info, prefixed);
 | 
			
		||||
			g_free(prefixed);
 | 
			
		||||
		}
 | 
			
		||||
		if ((name = g_file_info_get_edit_name(info))) {
 | 
			
		||||
		if (g_file_info_has_attribute(
 | 
			
		||||
				info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME) &&
 | 
			
		||||
			(name = g_file_info_get_edit_name(info))) {
 | 
			
		||||
			gchar *prefixed = get_prefixed_name(self, name);
 | 
			
		||||
			g_file_info_set_edit_name(info, prefixed);
 | 
			
		||||
			g_free(prefixed);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
//
 | 
			
		||||
// fiv-context-menu.c: popup menu
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -185,15 +185,24 @@ info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
 | 
			
		||||
	if (bytes_in)
 | 
			
		||||
		flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
 | 
			
		||||
 | 
			
		||||
	GSubprocessLauncher *launcher = g_subprocess_launcher_new(flags);
 | 
			
		||||
#ifdef G_OS_WIN32
 | 
			
		||||
	// Both to find wperl, and then to let wperl find the nearby exiftool.
 | 
			
		||||
	gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
 | 
			
		||||
	g_subprocess_launcher_set_cwd(launcher, prefix);
 | 
			
		||||
	g_free(prefix);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Add a fallback to internal capabilities.
 | 
			
		||||
	// The simplest is to specify the filename and the resolution.
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	GSubprocess *subprocess = g_subprocess_new(flags, &error,
 | 
			
		||||
	GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
 | 
			
		||||
#ifdef G_OS_WIN32
 | 
			
		||||
		"wperl",
 | 
			
		||||
#endif
 | 
			
		||||
		"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
 | 
			
		||||
		"--binary", "-quiet", "--", path, NULL);
 | 
			
		||||
	g_object_unref(launcher);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		info_redirect_error(dialog, error);
 | 
			
		||||
		return;
 | 
			
		||||
@@ -328,17 +337,13 @@ open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
open_context_show_error_dialog(OpenContext *self, GError *error)
 | 
			
		||||
show_error_dialog(GtkWindow *parent, GError *error)
 | 
			
		||||
{
 | 
			
		||||
	GtkWindow *window = g_weak_ref_get(&self->window);
 | 
			
		||||
 | 
			
		||||
	GtkWidget *dialog =
 | 
			
		||||
		gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL,
 | 
			
		||||
		gtk_message_dialog_new(GTK_WINDOW(parent), GTK_DIALOG_MODAL,
 | 
			
		||||
			GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
 | 
			
		||||
	gtk_dialog_run(GTK_DIALOG(dialog));
 | 
			
		||||
	gtk_widget_destroy(dialog);
 | 
			
		||||
 | 
			
		||||
	g_clear_object(&window);
 | 
			
		||||
	g_error_free(error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -357,7 +362,9 @@ open_context_launch(GtkWidget *widget, OpenContext *self)
 | 
			
		||||
		(void) g_app_info_set_as_last_used_for_type(
 | 
			
		||||
			self->app_info, self->content_type, NULL);
 | 
			
		||||
	} else {
 | 
			
		||||
		open_context_show_error_dialog(self, error);
 | 
			
		||||
		GtkWindow *window = g_weak_ref_get(&self->window);
 | 
			
		||||
		show_error_dialog(window, error);
 | 
			
		||||
		g_clear_object(&window);
 | 
			
		||||
	}
 | 
			
		||||
	g_list_free(files);
 | 
			
		||||
	g_object_unref(context);
 | 
			
		||||
@@ -437,14 +444,22 @@ on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
 | 
			
		||||
	g_free(uri);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_context_menu_remove(GtkWindow *parent, GFile *file)
 | 
			
		||||
{
 | 
			
		||||
	// TODO(p): Use g_file_trash_async(), for which we need a task manager.
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	if (!g_file_trash(file, NULL, &error))
 | 
			
		||||
		show_error_dialog(parent, error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
 | 
			
		||||
{
 | 
			
		||||
	// TODO(p): Use g_file_trash_async(), for which we need a task manager.
 | 
			
		||||
	OpenContext *ctx = user_data;
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	if (!g_file_trash(ctx->file, NULL, &error))
 | 
			
		||||
		open_context_show_error_dialog(ctx, error);
 | 
			
		||||
	GtkWindow *window = g_weak_ref_get(&ctx->window);
 | 
			
		||||
	fiv_context_menu_remove(window, ctx->file);
 | 
			
		||||
	g_clear_object(&window);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static gboolean
 | 
			
		||||
@@ -541,7 +556,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
 | 
			
		||||
		gtk_menu_shell_append(
 | 
			
		||||
			GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
			
		||||
 | 
			
		||||
		item = gtk_menu_item_new_with_mnemonic("_Information...");
 | 
			
		||||
		item = gtk_menu_item_new_with_mnemonic("_Information");
 | 
			
		||||
		g_signal_connect_data(item, "activate", G_CALLBACK(on_info_activate),
 | 
			
		||||
			g_rc_box_acquire(ctx), open_context_unref, 0);
 | 
			
		||||
		gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
//
 | 
			
		||||
// fiv-context-menu.h: popup menu
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -18,4 +18,5 @@
 | 
			
		||||
#include <gtk/gtk.h>
 | 
			
		||||
 | 
			
		||||
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
 | 
			
		||||
void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
 | 
			
		||||
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										463
									
								
								fiv-io-cmm.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										463
									
								
								fiv-io-cmm.c
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,463 @@
 | 
			
		||||
//
 | 
			
		||||
// fiv-io-cmm.c: colour management
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
//
 | 
			
		||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
			
		||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
			
		||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
			
		||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
			
		||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
#include "config.h"
 | 
			
		||||
 | 
			
		||||
#include <glib.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
 | 
			
		||||
#include "fiv-io.h"
 | 
			
		||||
 | 
			
		||||
// Colour management must be handled before RGB conversions.
 | 
			
		||||
// TODO(p): Make it also possible to use Skia's skcms.
 | 
			
		||||
#ifdef HAVE_LCMS2
 | 
			
		||||
#include <lcms2.h>
 | 
			
		||||
#endif  // HAVE_LCMS2
 | 
			
		||||
#ifdef HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
#include <lcms2_fast_float.h>
 | 
			
		||||
#endif  // HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
 | 
			
		||||
// --- CMM-independent transforms ----------------------------------------------
 | 
			
		||||
 | 
			
		||||
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
 | 
			
		||||
// ARGB/BGRA/XRGB/BGRX.
 | 
			
		||||
static void
 | 
			
		||||
trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
 | 
			
		||||
{
 | 
			
		||||
	// This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
 | 
			
		||||
	// It will typically produce horribly oversaturated results.
 | 
			
		||||
	// Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop
 | 
			
		||||
	// does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096
 | 
			
		||||
	while (len--) {
 | 
			
		||||
		int c = p[0], m = p[1], y = p[2], k = p[3];
 | 
			
		||||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
 | 
			
		||||
		p[0] = k * y / 255;
 | 
			
		||||
		p[1] = k * m / 255;
 | 
			
		||||
		p[2] = k * c / 255;
 | 
			
		||||
		p[3] = 255;
 | 
			
		||||
#else
 | 
			
		||||
		p[3] = k * y / 255;
 | 
			
		||||
		p[2] = k * m / 255;
 | 
			
		||||
		p[1] = k * c / 255;
 | 
			
		||||
		p[0] = 255;
 | 
			
		||||
#endif
 | 
			
		||||
		p += 4;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// From libwebp, verified to exactly match [x * a / 255].
 | 
			
		||||
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_premultiply_argb32(FivIoImage *image)
 | 
			
		||||
{
 | 
			
		||||
	if (image->format != CAIRO_FORMAT_ARGB32)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	for (uint32_t y = 0; y < image->height; y++) {
 | 
			
		||||
		uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
 | 
			
		||||
		for (uint32_t x = 0; x < image->width; x++) {
 | 
			
		||||
			uint32_t argb = dstp[x], a = argb >> 24;
 | 
			
		||||
			dstp[x] = a << 24 |
 | 
			
		||||
				PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
 | 
			
		||||
				PREMULTIPLY8(a, 0xFF & (argb >>  8)) <<  8 |
 | 
			
		||||
				PREMULTIPLY8(a, 0xFF &  argb);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Profiles ----------------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LCMS2
 | 
			
		||||
 | 
			
		||||
struct _FivIoProfile {
 | 
			
		||||
	FivIoCmm *cmm;
 | 
			
		||||
	cmsHPROFILE profile;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
GBytes *
 | 
			
		||||
fiv_io_profile_to_bytes(FivIoProfile *profile)
 | 
			
		||||
{
 | 
			
		||||
	cmsUInt32Number len = 0;
 | 
			
		||||
	(void) cmsSaveProfileToMem(profile, NULL, &len);
 | 
			
		||||
	gchar *data = g_malloc0(len);
 | 
			
		||||
	if (!cmsSaveProfileToMem(profile, data, &len)) {
 | 
			
		||||
		g_free(data);
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
	return g_bytes_new_take(data, len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static FivIoProfile *
 | 
			
		||||
fiv_io_profile_new(FivIoCmm *cmm, cmsHPROFILE profile)
 | 
			
		||||
{
 | 
			
		||||
	FivIoProfile *self = g_new0(FivIoProfile, 1);
 | 
			
		||||
	self->cmm = g_object_ref(cmm);
 | 
			
		||||
	self->profile = profile;
 | 
			
		||||
	return self;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_profile_free(FivIoProfile *self)
 | 
			
		||||
{
 | 
			
		||||
	cmsCloseProfile(self->profile);
 | 
			
		||||
	g_clear_object(&self->cmm);
 | 
			
		||||
	g_free(self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#else  // ! HAVE_LCMS2
 | 
			
		||||
 | 
			
		||||
GBytes *fiv_io_profile_to_bytes(FivIoProfile *) { return NULL; }
 | 
			
		||||
void fiv_io_profile_free(FivIoProfile *) {}
 | 
			
		||||
 | 
			
		||||
#endif  // ! HAVE_LCMS2
 | 
			
		||||
// --- Contexts ----------------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LCMS2
 | 
			
		||||
 | 
			
		||||
struct _FivIoCmm {
 | 
			
		||||
	GObject parent_instance;
 | 
			
		||||
	cmsContext context;
 | 
			
		||||
 | 
			
		||||
	// https://github.com/mm2/Little-CMS/issues/430
 | 
			
		||||
	gboolean broken_premul;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
G_DEFINE_TYPE(FivIoCmm, fiv_io_cmm, G_TYPE_OBJECT)
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_io_cmm_finalize(GObject *gobject)
 | 
			
		||||
{
 | 
			
		||||
	FivIoCmm *self = FIV_IO_CMM(gobject);
 | 
			
		||||
	cmsDeleteContext(self->context);
 | 
			
		||||
 | 
			
		||||
	G_OBJECT_CLASS(fiv_io_cmm_parent_class)->finalize(gobject);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_io_cmm_class_init(FivIoCmmClass *klass)
 | 
			
		||||
{
 | 
			
		||||
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
			
		||||
	object_class->finalize = fiv_io_cmm_finalize;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_io_cmm_init(FivIoCmm *self)
 | 
			
		||||
{
 | 
			
		||||
	self->context = cmsCreateContext(NULL, self);
 | 
			
		||||
#ifdef HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
	if (cmsPluginTHR(self->context, cmsFastFloatExtensions()))
 | 
			
		||||
		self->broken_premul = LCMS_VERSION <= 2160;
 | 
			
		||||
#endif  // HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
FivIoCmm *
 | 
			
		||||
fiv_io_cmm_get_default(void)
 | 
			
		||||
{
 | 
			
		||||
	static gsize initialization_value = 0;
 | 
			
		||||
	static FivIoCmm *default_ = NULL;
 | 
			
		||||
	if (g_once_init_enter(&initialization_value)) {
 | 
			
		||||
		gsize setup_value = 1;
 | 
			
		||||
		default_ = g_object_new(FIV_TYPE_IO_CMM, NULL);
 | 
			
		||||
		g_once_init_leave(&initialization_value, setup_value);
 | 
			
		||||
	}
 | 
			
		||||
	return default_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile(FivIoCmm *self, const void *data, size_t len)
 | 
			
		||||
{
 | 
			
		||||
	g_return_val_if_fail(self != NULL, NULL);
 | 
			
		||||
 | 
			
		||||
	return fiv_io_profile_new(self,
 | 
			
		||||
		cmsOpenProfileFromMemTHR(self->context, data, len));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile_sRGB(FivIoCmm *self)
 | 
			
		||||
{
 | 
			
		||||
	g_return_val_if_fail(self != NULL, NULL);
 | 
			
		||||
 | 
			
		||||
	return fiv_io_profile_new(self,
 | 
			
		||||
		cmsCreate_sRGBProfileTHR(self->context));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile_parametric(FivIoCmm *self,
 | 
			
		||||
	double gamma, double whitepoint[2], double primaries[6])
 | 
			
		||||
{
 | 
			
		||||
	g_return_val_if_fail(self != NULL, NULL);
 | 
			
		||||
 | 
			
		||||
	const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0};
 | 
			
		||||
	const cmsCIExyYTRIPLE cmsP = {
 | 
			
		||||
		{primaries[0], primaries[1], 1.0},
 | 
			
		||||
		{primaries[2], primaries[3], 1.0},
 | 
			
		||||
		{primaries[4], primaries[5], 1.0},
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	cmsToneCurve *curve = cmsBuildGamma(self->context, gamma);
 | 
			
		||||
	if (!curve)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	cmsHPROFILE profile = cmsCreateRGBProfileTHR(self->context,
 | 
			
		||||
		&cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
 | 
			
		||||
	cmsFreeToneCurve(curve);
 | 
			
		||||
	return fiv_io_profile_new(self, profile);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#else  // ! HAVE_LCMS2
 | 
			
		||||
 | 
			
		||||
FivIoCmm *
 | 
			
		||||
fiv_io_cmm_get_default()
 | 
			
		||||
{
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile(FivIoCmm *, const void *, size_t)
 | 
			
		||||
{
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile_sRGB(FivIoCmm *)
 | 
			
		||||
{
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile_parametric(FivIoCmm *, double, double[2], double[6])
 | 
			
		||||
{
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // ! HAVE_LCMS2
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma)
 | 
			
		||||
{
 | 
			
		||||
	return fiv_io_cmm_get_profile_parametric(self, gamma,
 | 
			
		||||
		(double[2]){0.3127, 0.3290},
 | 
			
		||||
		(double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FivIoProfile *
 | 
			
		||||
fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes)
 | 
			
		||||
{
 | 
			
		||||
	gsize len = 0;
 | 
			
		||||
	gconstpointer p = g_bytes_get_data(bytes, &len);
 | 
			
		||||
	return fiv_io_cmm_get_profile(self, p, len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Image loading -----------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LCMS2
 | 
			
		||||
 | 
			
		||||
// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
 | 
			
		||||
#define FIV_IO_PROFILE_ARGB32 \
 | 
			
		||||
	(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
 | 
			
		||||
#define FIV_IO_PROFILE_4X16LE \
 | 
			
		||||
	(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_cmyk(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	g_return_if_fail(target == NULL || self != NULL);
 | 
			
		||||
 | 
			
		||||
	cmsHTRANSFORM transform = NULL;
 | 
			
		||||
	if (source && target) {
 | 
			
		||||
		transform = cmsCreateTransformTHR(self->context,
 | 
			
		||||
			source->profile, TYPE_CMYK_8_REV,
 | 
			
		||||
			target->profile, FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
 | 
			
		||||
	}
 | 
			
		||||
	if (transform) {
 | 
			
		||||
		cmsDoTransform(
 | 
			
		||||
			transform, image->data, image->data, image->width * image->height);
 | 
			
		||||
		cmsDeleteTransform(transform);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	trivial_cmyk_to_host_byte_order_argb(
 | 
			
		||||
		image->data, image->width * image->height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool
 | 
			
		||||
fiv_io_cmm_rgb_direct(FivIoCmm *self, unsigned char *data, int w, int h,
 | 
			
		||||
	FivIoProfile *source, FivIoProfile *target,
 | 
			
		||||
	uint32_t source_format, uint32_t target_format)
 | 
			
		||||
{
 | 
			
		||||
	g_return_val_if_fail(target == NULL || self != NULL, false);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): We should make this optional.
 | 
			
		||||
	FivIoProfile *src_fallback = NULL;
 | 
			
		||||
	if (target && !source)
 | 
			
		||||
		source = src_fallback = fiv_io_cmm_get_profile_sRGB(self);
 | 
			
		||||
 | 
			
		||||
	cmsHTRANSFORM transform = NULL;
 | 
			
		||||
	if (source && target) {
 | 
			
		||||
		transform = cmsCreateTransformTHR(self->context,
 | 
			
		||||
			source->profile, source_format,
 | 
			
		||||
			target->profile, target_format, INTENT_PERCEPTUAL, 0);
 | 
			
		||||
	}
 | 
			
		||||
	if (transform) {
 | 
			
		||||
		cmsDoTransform(transform, data, data, w * h);
 | 
			
		||||
		cmsDeleteTransform(transform);
 | 
			
		||||
	}
 | 
			
		||||
	if (src_fallback)
 | 
			
		||||
		fiv_io_profile_free(src_fallback);
 | 
			
		||||
	return transform != NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_io_cmm_xrgb32(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
 | 
			
		||||
		source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
 | 
			
		||||
	int w, int h, FivIoProfile *source, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	fiv_io_cmm_rgb_direct(self, data, w, h, source, target,
 | 
			
		||||
		FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#else  // ! HAVE_LCMS2
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_cmyk(FivIoCmm *, FivIoImage *image, FivIoProfile *, FivIoProfile *)
 | 
			
		||||
{
 | 
			
		||||
	trivial_cmyk_to_host_byte_order_argb(
 | 
			
		||||
		image->data, image->width * image->height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_io_cmm_xrgb32(FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *)
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_4x16le_direct(
 | 
			
		||||
	FivIoCmm *, unsigned char *, int, int, FivIoProfile *, FivIoProfile *)
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // ! HAVE_LCMS2
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
 | 
			
		||||
 | 
			
		||||
#define FIV_IO_PROFILE_ARGB32_PREMUL \
 | 
			
		||||
	(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_io_cmm_argb32(FivIoCmm *self, FivIoImage *image,
 | 
			
		||||
	FivIoProfile *source, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
 | 
			
		||||
 | 
			
		||||
	// TODO: With self->broken_premul,
 | 
			
		||||
	// this probably also needs to be wrapped in un-premultiplication.
 | 
			
		||||
	fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
 | 
			
		||||
		source, target,
 | 
			
		||||
		FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	g_return_if_fail(target == NULL || self != NULL);
 | 
			
		||||
 | 
			
		||||
	if (image->format != CAIRO_FORMAT_ARGB32) {
 | 
			
		||||
		fiv_io_cmm_xrgb32(self, image, source, target);
 | 
			
		||||
	} else if (!target || self->broken_premul) {
 | 
			
		||||
		fiv_io_cmm_xrgb32(self, image, source, target);
 | 
			
		||||
		fiv_io_premultiply_argb32(image);
 | 
			
		||||
	} else if (!fiv_io_cmm_rgb_direct(self, image->data,
 | 
			
		||||
			image->width, image->height, source, target,
 | 
			
		||||
			FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
 | 
			
		||||
		g_debug("failed to create a premultiplying transform");
 | 
			
		||||
		fiv_io_premultiply_argb32(image);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#else  // ! HAVE_LCMS2 || LCMS_VERSION < 2130
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_io_cmm_argb32(G_GNUC_UNUSED FivIoCmm *self, G_GNUC_UNUSED FivIoImage *image,
 | 
			
		||||
	G_GNUC_UNUSED FivIoProfile *source, G_GNUC_UNUSED FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	fiv_io_cmm_xrgb32(self, image, source, target);
 | 
			
		||||
	fiv_io_premultiply_argb32(image);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // ! HAVE_LCMS2 || LCMS_VERSION < 2130
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
 | 
			
		||||
	void (*frame_cb) (FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *))
 | 
			
		||||
{
 | 
			
		||||
	FivIoProfile *source = NULL;
 | 
			
		||||
	if (page->icc)
 | 
			
		||||
		source = fiv_io_cmm_get_profile_from_bytes(self, page->icc);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): All animations need to be composited in a linear colour space.
 | 
			
		||||
	for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
 | 
			
		||||
		frame_cb(self, frame, source, target);
 | 
			
		||||
 | 
			
		||||
	if (source)
 | 
			
		||||
		fiv_io_profile_free(source);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
fiv_io_cmm_any(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	// TODO(p): Ensure we do colour management early enough, so that
 | 
			
		||||
	// no avoidable increase of quantization error occurs beforehands,
 | 
			
		||||
	// and also for correct alpha compositing.
 | 
			
		||||
	switch (image->format) {
 | 
			
		||||
	break; case CAIRO_FORMAT_RGB24:
 | 
			
		||||
		fiv_io_cmm_xrgb32(self, image, source, target);
 | 
			
		||||
	break; case CAIRO_FORMAT_ARGB32:
 | 
			
		||||
		fiv_io_cmm_argb32(self, image, source, target);
 | 
			
		||||
	break; default:
 | 
			
		||||
		g_debug("CM attempted on an unsupported surface format");
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
 | 
			
		||||
FivIoImage *
 | 
			
		||||
fiv_io_cmm_finish(FivIoCmm *self, FivIoImage *image, FivIoProfile *target)
 | 
			
		||||
{
 | 
			
		||||
	if (!target)
 | 
			
		||||
		return image;
 | 
			
		||||
 | 
			
		||||
	for (FivIoImage *page = image; page != NULL; page = page->page_next)
 | 
			
		||||
		fiv_io_cmm_page(self, page, target, fiv_io_cmm_any);
 | 
			
		||||
	return image;
 | 
			
		||||
}
 | 
			
		||||
@@ -166,6 +166,10 @@ model_entry_array_new(void)
 | 
			
		||||
		(GDestroyNotify) fiv_io_model_entry_unref);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#if !GLIB_CHECK_VERSION(2, 70, 0)
 | 
			
		||||
#define g_pattern_spec_match g_pattern_match
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
static gboolean
 | 
			
		||||
model_supports(FivIoModel *self, const char *filename)
 | 
			
		||||
{
 | 
			
		||||
@@ -243,7 +247,9 @@ static GPtrArray *
 | 
			
		||||
model_decide_placement(
 | 
			
		||||
	FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
 | 
			
		||||
{
 | 
			
		||||
	if (self->filtering && g_file_info_get_is_hidden(info))
 | 
			
		||||
	if (self->filtering &&
 | 
			
		||||
		g_file_info_has_attribute(info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
 | 
			
		||||
		g_file_info_get_is_hidden(info))
 | 
			
		||||
		return NULL;
 | 
			
		||||
	if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
 | 
			
		||||
		return subdirs;
 | 
			
		||||
@@ -342,6 +348,8 @@ static void
 | 
			
		||||
monitor_apply(enum monitor_event event, GPtrArray *target, int index,
 | 
			
		||||
	FivIoModelEntry *new_entry)
 | 
			
		||||
{
 | 
			
		||||
	g_return_if_fail(event != MONITOR_CHANGING || index >= 0);
 | 
			
		||||
 | 
			
		||||
	if (event == MONITOR_RENAMING && index < 0)
 | 
			
		||||
		// The file used to be filtered out but isn't anymore.
 | 
			
		||||
		event = MONITOR_ADDING;
 | 
			
		||||
@@ -702,7 +710,7 @@ fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
 | 
			
		||||
 | 
			
		||||
	GError *e = NULL;
 | 
			
		||||
	if ((self->monitor = g_file_monitor_directory(
 | 
			
		||||
			 directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
 | 
			
		||||
			directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
 | 
			
		||||
		g_signal_connect(self->monitor, "changed",
 | 
			
		||||
			G_CALLBACK(on_monitor_changed), self);
 | 
			
		||||
	} else {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										242
									
								
								fiv-io.h
									
									
									
									
									
								
							
							
						
						
									
										242
									
								
								fiv-io.h
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
//
 | 
			
		||||
// fiv-io.h: image operations
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -22,16 +22,53 @@
 | 
			
		||||
#include <glib.h>
 | 
			
		||||
#include <webp/encode.h>  // WebPConfig
 | 
			
		||||
 | 
			
		||||
typedef enum _FivIoOrientation FivIoOrientation;
 | 
			
		||||
typedef struct _FivIoRenderClosure FivIoRenderClosure;
 | 
			
		||||
typedef struct _FivIoImage FivIoImage;
 | 
			
		||||
typedef struct _FivIoProfile FivIoProfile;
 | 
			
		||||
 | 
			
		||||
// --- Colour management -------------------------------------------------------
 | 
			
		||||
// Note that without a CMM, all FivIoCmm and FivIoProfile will be returned NULL.
 | 
			
		||||
 | 
			
		||||
// TODO(p): Make it also possible to use Skia's skcms.
 | 
			
		||||
typedef void *FivIoProfile;
 | 
			
		||||
FivIoProfile fiv_io_profile_new(const void *data, size_t len);
 | 
			
		||||
FivIoProfile fiv_io_profile_new_sRGB(void);
 | 
			
		||||
void fiv_io_profile_free(FivIoProfile self);
 | 
			
		||||
GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
 | 
			
		||||
void fiv_io_profile_free(FivIoProfile *self);
 | 
			
		||||
 | 
			
		||||
// From libwebp, verified to exactly match [x * a / 255].
 | 
			
		||||
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
#define FIV_TYPE_IO_CMM (fiv_io_cmm_get_type())
 | 
			
		||||
G_DECLARE_FINAL_TYPE(FivIoCmm, fiv_io_cmm, FIV, IO_CMM, GObject)
 | 
			
		||||
 | 
			
		||||
FivIoCmm *fiv_io_cmm_get_default(void);
 | 
			
		||||
 | 
			
		||||
FivIoProfile *fiv_io_cmm_get_profile(
 | 
			
		||||
	FivIoCmm *self, const void *data, size_t len);
 | 
			
		||||
FivIoProfile *fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes);
 | 
			
		||||
FivIoProfile *fiv_io_cmm_get_profile_sRGB(FivIoCmm *self);
 | 
			
		||||
FivIoProfile *fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma);
 | 
			
		||||
FivIoProfile *fiv_io_cmm_get_profile_parametric(
 | 
			
		||||
	FivIoCmm *self, double gamma, double whitepoint[2], double primaries[6]);
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
void fiv_io_premultiply_argb32(FivIoImage *image);
 | 
			
		||||
 | 
			
		||||
void fiv_io_cmm_cmyk(FivIoCmm *self,
 | 
			
		||||
    FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
 | 
			
		||||
void fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
 | 
			
		||||
    int w, int h, FivIoProfile *source, FivIoProfile *target);
 | 
			
		||||
 | 
			
		||||
void fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
 | 
			
		||||
#define fiv_io_cmm_argb32_premultiply_page(cmm, page, target) \
 | 
			
		||||
	fiv_io_cmm_page((cmm), (page), (target), fiv_io_cmm_argb32_premultiply)
 | 
			
		||||
 | 
			
		||||
void fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
 | 
			
		||||
	void (*frame_cb) (FivIoCmm *,
 | 
			
		||||
		FivIoImage *, FivIoProfile *, FivIoProfile *));
 | 
			
		||||
void fiv_io_cmm_any(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
 | 
			
		||||
FivIoImage *fiv_io_cmm_finish(FivIoCmm *self,
 | 
			
		||||
	FivIoImage *image, FivIoProfile *target);
 | 
			
		||||
 | 
			
		||||
// --- Loading -----------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
@@ -39,64 +76,126 @@ extern const char *fiv_io_supported_media_types[];
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
extern cairo_user_data_key_t fiv_io_key_exif;
 | 
			
		||||
/// FivIoOrientation, as a uintptr_t.
 | 
			
		||||
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 {
 | 
			
		||||
// TODO(p): Maybe make FivIoProfile a referencable type,
 | 
			
		||||
// then loaders could store it in their closures.
 | 
			
		||||
struct _FivIoRenderClosure {
 | 
			
		||||
	/// The rendering is allowed to fail, returning NULL.
 | 
			
		||||
	cairo_surface_t *(*render)(struct _FivIoRenderClosure *, double scale);
 | 
			
		||||
} FivIoRenderClosure;
 | 
			
		||||
	FivIoImage *(*render)(
 | 
			
		||||
		FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
 | 
			
		||||
	void (*destroy)(FivIoRenderClosure *);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// A FivIoRenderClosure for parametrized re-rendering of vector formats.
 | 
			
		||||
/// This is attached at the page level.
 | 
			
		||||
/// The rendered image will not have this key.
 | 
			
		||||
extern cairo_user_data_key_t fiv_io_key_render;
 | 
			
		||||
// Metadata are typically attached to all Cairo surfaces in an animation.
 | 
			
		||||
 | 
			
		||||
struct _FivIoImage {
 | 
			
		||||
	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 {
 | 
			
		||||
	const char *uri;                    ///< Source URI
 | 
			
		||||
	FivIoProfile screen_profile;        ///< Target colour space or NULL
 | 
			
		||||
	FivIoCmm *cmm;                      ///< Colour management module or NULL
 | 
			
		||||
	FivIoProfile *screen_profile;       ///< Target colour space or NULL
 | 
			
		||||
	int screen_dpi;                     ///< Target DPI
 | 
			
		||||
	gboolean enhance;                   ///< Enhance JPEG (currently)
 | 
			
		||||
	gboolean first_frame_only;          ///< Only interested in the 1st frame
 | 
			
		||||
	GPtrArray *warnings;                ///< String vector for non-fatal errors
 | 
			
		||||
} FivIoOpenContext;
 | 
			
		||||
 | 
			
		||||
cairo_surface_t *fiv_io_open(const FivIoOpenContext *ctx, GError **error);
 | 
			
		||||
cairo_surface_t *fiv_io_open_from_data(
 | 
			
		||||
FivIoImage *fiv_io_open(const FivIoOpenContext *ctx, GError **error);
 | 
			
		||||
FivIoImage *fiv_io_open_from_data(
 | 
			
		||||
	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 ---------------------------------------------
 | 
			
		||||
 | 
			
		||||
@@ -109,41 +208,12 @@ GBytes *fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error);
 | 
			
		||||
 | 
			
		||||
// --- 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().
 | 
			
		||||
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.
 | 
			
		||||
/// If no exact frame is specified, this potentially creates an animation.
 | 
			
		||||
gboolean fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame,
 | 
			
		||||
	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);
 | 
			
		||||
gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
 | 
			
		||||
	FivIoProfile *target, const char *path, GError **error);
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,9 @@
 | 
			
		||||
#include <gtk/gtk.h>
 | 
			
		||||
#include <turbojpeg.h>
 | 
			
		||||
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
 | 
			
		||||
#include "config.h"
 | 
			
		||||
 | 
			
		||||
// --- Utilities ---------------------------------------------------------------
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,5 @@ if [ "$#" -ne 2 ]; then
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
 | 
			
		||||
	| curl --silent --show-error --upload-file - https://transfer.sh/image \
 | 
			
		||||
	| jq --slurp --raw-input --raw-output @uri)"
 | 
			
		||||
	| curl --silent --show-error --form 'files[]=@-' https://uguu.se/upload \
 | 
			
		||||
	| jq --raw-output '.files[] | .url | @uri')"
 | 
			
		||||
 
 | 
			
		||||
@@ -433,7 +433,10 @@ complete_path(GFile *location, GtkListStore *model)
 | 
			
		||||
			!info)
 | 
			
		||||
			break;
 | 
			
		||||
 | 
			
		||||
		if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY ||
 | 
			
		||||
		if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY)
 | 
			
		||||
			continue;
 | 
			
		||||
		if (g_file_info_has_attribute(info,
 | 
			
		||||
				G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
 | 
			
		||||
			g_file_info_get_is_hidden(info))
 | 
			
		||||
			continue;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										243
									
								
								fiv-thumbnail.c
									
									
									
									
									
								
							
							
						
						
									
										243
									
								
								fiv-thumbnail.c
									
									
									
									
									
								
							@@ -100,7 +100,16 @@ mark_thumbnail_lq(cairo_surface_t *surface)
 | 
			
		||||
static gchar *
 | 
			
		||||
fiv_thumbnail_get_root(void)
 | 
			
		||||
{
 | 
			
		||||
#ifdef G_OS_WIN32
 | 
			
		||||
	// We can do better than GLib with FOLDERID_InternetCache,
 | 
			
		||||
	// and we don't want to place .cache directly in the user's home.
 | 
			
		||||
	// TODO(p): Register this thumbnail path using the installer:
 | 
			
		||||
	// https://learn.microsoft.com/en-us/windows/win32/lwef/disk-cleanup
 | 
			
		||||
	gchar *cache_dir =
 | 
			
		||||
		g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
 | 
			
		||||
#else
 | 
			
		||||
	gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
 | 
			
		||||
#endif
 | 
			
		||||
	gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL);
 | 
			
		||||
	g_free(cache_dir);
 | 
			
		||||
	return thumbnails_dir;
 | 
			
		||||
@@ -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)
 | 
			
		||||
{
 | 
			
		||||
	FivIoCmm *cmm = fiv_io_cmm_get_default();
 | 
			
		||||
	FivIoOpenContext ctx = {
 | 
			
		||||
		.uri = g_file_get_uri(target),
 | 
			
		||||
		.screen_profile = fiv_io_profile_new_sRGB(),
 | 
			
		||||
		// Remember to synchronize changes with adjust_thumbnail().
 | 
			
		||||
		.cmm = cmm,
 | 
			
		||||
		.screen_profile = fiv_io_cmm_get_profile_sRGB(cmm),
 | 
			
		||||
		.screen_dpi = 96,
 | 
			
		||||
		.first_frame_only = TRUE,
 | 
			
		||||
		// Only using this array as a redirect.
 | 
			
		||||
		.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_free((gchar *) ctx.uri);
 | 
			
		||||
	g_ptr_array_free(ctx.warnings, TRUE);
 | 
			
		||||
	if ((*color_managed = !!ctx.screen_profile))
 | 
			
		||||
		fiv_io_profile_free(ctx.screen_profile);
 | 
			
		||||
	g_bytes_unref(data);
 | 
			
		||||
	return surface;
 | 
			
		||||
	return image;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// In principle similar to rescale_thumbnail() from fiv-browser.c.
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
 | 
			
		||||
static FivIoImage *
 | 
			
		||||
adjust_thumbnail(FivIoImage *thumbnail, double row_height)
 | 
			
		||||
{
 | 
			
		||||
	// Hardcode orientation.
 | 
			
		||||
	FivIoOrientation orientation = (uintptr_t) cairo_surface_get_user_data(
 | 
			
		||||
		thumbnail, &fiv_io_key_orientation);
 | 
			
		||||
	FivIoOrientation orientation = thumbnail->orientation;
 | 
			
		||||
 | 
			
		||||
	double w = 0, h = 0;
 | 
			
		||||
	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.
 | 
			
		||||
	FivIoRenderClosure *closure =
 | 
			
		||||
		cairo_surface_get_user_data(thumbnail, &fiv_io_key_render);
 | 
			
		||||
	FivIoRenderClosure *closure = thumbnail->render;
 | 
			
		||||
	if (closure && orientation <= FivIoOrientation0) {
 | 
			
		||||
		// Remember to synchronize changes with render().
 | 
			
		||||
		FivIoCmm *cmm = fiv_io_cmm_get_default();
 | 
			
		||||
		FivIoProfile *screen_profile = fiv_io_cmm_get_profile_sRGB(cmm);
 | 
			
		||||
		// This API doesn't accept non-uniform scaling; prefer a vertical fit.
 | 
			
		||||
		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)
 | 
			
		||||
			return scaled;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// This will be CAIRO_FORMAT_INVALID with non-image surfaces, which is fine.
 | 
			
		||||
	cairo_format_t format = cairo_image_surface_get_format(thumbnail);
 | 
			
		||||
	if (format != CAIRO_FORMAT_INVALID &&
 | 
			
		||||
		orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
 | 
			
		||||
		return cairo_surface_reference(thumbnail);
 | 
			
		||||
	if (orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
 | 
			
		||||
		return fiv_io_image_ref(thumbnail);
 | 
			
		||||
 | 
			
		||||
	cairo_format_t format = thumbnail->format;
 | 
			
		||||
	int projected_width = round(scale_x * w);
 | 
			
		||||
	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)
 | 
			
		||||
			? CAIRO_FORMAT_RGB24
 | 
			
		||||
			: CAIRO_FORMAT_ARGB32,
 | 
			
		||||
		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_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_FILTER_BEST, for some reason, works bad with CAIRO_FORMAT_RGB30.
 | 
			
		||||
	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,
 | 
			
		||||
	// even though nothing will be rendered.
 | 
			
		||||
	if (cairo_surface_status(thumbnail) != CAIRO_STATUS_SUCCESS ||
 | 
			
		||||
		cairo_surface_status(scaled) != CAIRO_STATUS_SUCCESS ||
 | 
			
		||||
		cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
 | 
			
		||||
	if (cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
 | 
			
		||||
		cairo_status(cr) != CAIRO_STATUS_SUCCESS)
 | 
			
		||||
		g_warning("thumbnail scaling failed");
 | 
			
		||||
 | 
			
		||||
@@ -218,27 +240,32 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
 | 
			
		||||
	return scaled;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
orient_thumbnail(cairo_surface_t *surface)
 | 
			
		||||
static FivIoImage *
 | 
			
		||||
orient_thumbnail(FivIoImage *image)
 | 
			
		||||
{
 | 
			
		||||
	int orientation = (intptr_t) cairo_surface_get_user_data(
 | 
			
		||||
		surface, &fiv_io_key_orientation);
 | 
			
		||||
	if (orientation <= FivIoOrientation0)
 | 
			
		||||
		return surface;
 | 
			
		||||
	if (image->orientation <= FivIoOrientation0)
 | 
			
		||||
		return image;
 | 
			
		||||
 | 
			
		||||
	double w = 0, h = 0;
 | 
			
		||||
	cairo_matrix_t matrix =
 | 
			
		||||
		fiv_io_orientation_apply(surface, orientation, &w, &h);
 | 
			
		||||
	cairo_surface_t *oriented =
 | 
			
		||||
		cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
 | 
			
		||||
		fiv_io_orientation_apply(image, image->orientation, &w, &h);
 | 
			
		||||
	FivIoImage *oriented = fiv_io_image_new(image->format, 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_surface_destroy(surface);
 | 
			
		||||
	cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
 | 
			
		||||
	cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
 | 
			
		||||
	cairo_paint(cr);
 | 
			
		||||
	cairo_destroy(cr);
 | 
			
		||||
	cairo_surface_destroy(surface);
 | 
			
		||||
	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.
 | 
			
		||||
	// TODO(p): Maybe don't mark raw image thumbnails as low-quality
 | 
			
		||||
	// 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;
 | 
			
		||||
	// Note that the ratio may even be larger than 1, as seen with CR2 files.
 | 
			
		||||
	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)
 | 
			
		||||
		i++;
 | 
			
		||||
 | 
			
		||||
	if (i < count)
 | 
			
		||||
	bool found = i != count;
 | 
			
		||||
	if (found)
 | 
			
		||||
		i = sorted[i] - iprc->thumbs_list.thumblist;
 | 
			
		||||
 | 
			
		||||
	g_free(sorted);
 | 
			
		||||
	if (i == count) {
 | 
			
		||||
	if (!found) {
 | 
			
		||||
		set_error(error, "no suitable thumbnails found");
 | 
			
		||||
		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.:
 | 
			
		||||
	//  - Phase One/H 25/H25_Outdoor_.IIQ
 | 
			
		||||
	//  - 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -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)
 | 
			
		||||
{
 | 
			
		||||
	// Anything else is extremely rare.
 | 
			
		||||
@@ -398,24 +426,26 @@ extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cairo_surface_t *surface = cairo_image_surface_create(
 | 
			
		||||
	FivIoImage *I = fiv_io_image_new(
 | 
			
		||||
		CAIRO_FORMAT_RGB24, image->width, image->height);
 | 
			
		||||
	guint32 *out = (guint32 *) cairo_image_surface_get_data(surface);
 | 
			
		||||
	const unsigned char *in = image->data;
 | 
			
		||||
	for (guint64 i = 0; i < image->width * image->height; in += 3)
 | 
			
		||||
		out[i++] = in[0] << 16 | in[1] << 8 | in[2];
 | 
			
		||||
	cairo_surface_mark_dirty(surface);
 | 
			
		||||
	if (!I) {
 | 
			
		||||
		set_error(error, "image allocation failure");
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	FivIoOrientation orient = extract_libraw_unflip(flip);
 | 
			
		||||
	cairo_surface_set_user_data(
 | 
			
		||||
		surface, &fiv_io_key_orientation, (void *) (intptr_t) orient, NULL);
 | 
			
		||||
	return surface;
 | 
			
		||||
	guint32 *out = (guint32 *) I->data;
 | 
			
		||||
	const unsigned char *in = image->data;
 | 
			
		||||
	for (guint64 i = 0; i < (guint64) image->width * image->height; in += 3)
 | 
			
		||||
		out[i++] = in[0] << 16 | in[1] << 8 | in[2];
 | 
			
		||||
 | 
			
		||||
	I->orientation = extract_libraw_unflip(flip);
 | 
			
		||||
	return I;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
static FivIoImage *
 | 
			
		||||
extract_libraw(GFile *target, GMappedFile *mf, GError **error)
 | 
			
		||||
{
 | 
			
		||||
	cairo_surface_t *surface = NULL;
 | 
			
		||||
	FivIoImage *I = NULL;
 | 
			
		||||
	libraw_data_t *iprc = libraw_init(
 | 
			
		||||
		LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
 | 
			
		||||
	if (!iprc) {
 | 
			
		||||
@@ -457,7 +487,8 @@ extract_libraw(GFile *target, GMappedFile *mf, GError **error)
 | 
			
		||||
	//  - Samsung/NX200/2013-05-08-194524__sam6589.srw
 | 
			
		||||
	//  - Sony/DSC-HX95/DSC00018.ARW
 | 
			
		||||
	// 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:
 | 
			
		||||
	//  - 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) {
 | 
			
		||||
		gboolean dummy;
 | 
			
		||||
	case LIBRAW_IMAGE_JPEG:
 | 
			
		||||
		surface = render(
 | 
			
		||||
		I = render(
 | 
			
		||||
			target, g_bytes_new(image->data, image->data_size), &dummy, error);
 | 
			
		||||
		break;
 | 
			
		||||
	case LIBRAW_IMAGE_BITMAP:
 | 
			
		||||
		surface = extract_libraw_bitmap(image, flip, error);
 | 
			
		||||
		I = extract_libraw_bitmap(image, flip, error);
 | 
			
		||||
		break;
 | 
			
		||||
	default:
 | 
			
		||||
		set_error(error, "unsupported embedded thumbnail");
 | 
			
		||||
@@ -478,7 +509,7 @@ extract_libraw(GFile *target, GMappedFile *mf, GError **error)
 | 
			
		||||
	libraw_dcraw_clear_mem(image);
 | 
			
		||||
fail:
 | 
			
		||||
	libraw_close(iprc);
 | 
			
		||||
	return surface;
 | 
			
		||||
	return I;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // HAVE_LIBRAW
 | 
			
		||||
@@ -504,30 +535,30 @@ fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cairo_surface_t *surface = NULL;
 | 
			
		||||
	FivIoImage *image = NULL;
 | 
			
		||||
#ifdef HAVE_LIBRAW
 | 
			
		||||
	surface = extract_libraw(target, mf, error);
 | 
			
		||||
	image = extract_libraw(target, mf, error);
 | 
			
		||||
#else  // ! HAVE_LIBRAW
 | 
			
		||||
	// TODO(p): Implement our own thumbnail extractors.
 | 
			
		||||
	set_error(error, "unsupported file");
 | 
			
		||||
#endif  // ! HAVE_LIBRAW
 | 
			
		||||
	g_mapped_file_unref(mf);
 | 
			
		||||
 | 
			
		||||
	if (!surface)
 | 
			
		||||
	if (!image)
 | 
			
		||||
		return NULL;
 | 
			
		||||
	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 =
 | 
			
		||||
		adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
 | 
			
		||||
	cairo_surface_destroy(surface);
 | 
			
		||||
	return result;
 | 
			
		||||
	FivIoImage *result =
 | 
			
		||||
		adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
 | 
			
		||||
	fiv_io_image_unref(image);
 | 
			
		||||
	return fiv_io_image_to_surface(result);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
static WebPData
 | 
			
		||||
encode_thumbnail(cairo_surface_t *surface)
 | 
			
		||||
encode_thumbnail(FivIoImage *image)
 | 
			
		||||
{
 | 
			
		||||
	WebPData bitstream = {};
 | 
			
		||||
	WebPConfig config = {};
 | 
			
		||||
@@ -539,12 +570,12 @@ encode_thumbnail(cairo_surface_t *surface)
 | 
			
		||||
	if (!WebPValidateConfig(&config))
 | 
			
		||||
		return bitstream;
 | 
			
		||||
 | 
			
		||||
	bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size);
 | 
			
		||||
	bitstream.bytes = fiv_io_encode_webp(image, &config, &bitstream.size);
 | 
			
		||||
	return bitstream;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
	WebPData bitstream = encode_thumbnail(thumbnail);
 | 
			
		||||
@@ -600,15 +631,15 @@ fiv_thumbnail_produce_for_search(
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	gboolean color_managed = FALSE;
 | 
			
		||||
	cairo_surface_t *surface = render(target, data, &color_managed, error);
 | 
			
		||||
	if (!surface)
 | 
			
		||||
	FivIoImage *image = render(target, data, &color_managed, error);
 | 
			
		||||
	if (!image)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Might want to keep this a square.
 | 
			
		||||
	cairo_surface_t *result =
 | 
			
		||||
		adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
 | 
			
		||||
	cairo_surface_destroy(surface);
 | 
			
		||||
	return result;
 | 
			
		||||
	FivIoImage *result =
 | 
			
		||||
		adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
 | 
			
		||||
	fiv_io_image_unref(image);
 | 
			
		||||
	return fiv_io_image_to_surface(result);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
@@ -636,14 +667,14 @@ produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	gboolean color_managed = FALSE;
 | 
			
		||||
	cairo_surface_t *surface = render(target, data, &color_managed, error);
 | 
			
		||||
	if (!surface)
 | 
			
		||||
	FivIoImage *image = render(target, data, &color_managed, error);
 | 
			
		||||
	if (!image)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	cairo_surface_t *result =
 | 
			
		||||
		adjust_thumbnail(surface, fiv_thumbnail_sizes[size].size);
 | 
			
		||||
	cairo_surface_destroy(surface);
 | 
			
		||||
	return result;
 | 
			
		||||
	FivIoImage *result =
 | 
			
		||||
		adjust_thumbnail(image, fiv_thumbnail_sizes[size].size);
 | 
			
		||||
	fiv_io_image_unref(image);
 | 
			
		||||
	return fiv_io_image_to_surface(result);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
cairo_surface_t *
 | 
			
		||||
@@ -688,10 +719,10 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gboolean color_managed = FALSE;
 | 
			
		||||
	cairo_surface_t *surface =
 | 
			
		||||
	FivIoImage *image =
 | 
			
		||||
		render(target, g_mapped_file_get_bytes(mf), &color_managed, error);
 | 
			
		||||
	g_mapped_file_unref(mf);
 | 
			
		||||
	if (!surface)
 | 
			
		||||
	if (!image)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	// Boilerplate copied from fiv_thumbnail_lookup().
 | 
			
		||||
@@ -707,12 +738,10 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
 | 
			
		||||
	g_string_append_printf(
 | 
			
		||||
		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%d%c", THUMB_IMAGE_WIDTH, 0,
 | 
			
		||||
			cairo_image_surface_get_width(surface), 0);
 | 
			
		||||
		g_string_append_printf(thum, "%s%c%d%c", THUMB_IMAGE_HEIGHT, 0,
 | 
			
		||||
			cairo_image_surface_get_height(surface), 0);
 | 
			
		||||
	}
 | 
			
		||||
	g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_WIDTH, 0,
 | 
			
		||||
		(unsigned) image->width, 0);
 | 
			
		||||
	g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_HEIGHT, 0,
 | 
			
		||||
		(unsigned) image->height, 0);
 | 
			
		||||
 | 
			
		||||
	// Without a CMM, no conversion is attempted.
 | 
			
		||||
	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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cairo_surface_t *max_size_surface = NULL;
 | 
			
		||||
	FivIoImage *max_size_image = NULL;
 | 
			
		||||
	for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) {
 | 
			
		||||
		cairo_surface_t *scaled =
 | 
			
		||||
			adjust_thumbnail(surface, fiv_thumbnail_sizes[use].size);
 | 
			
		||||
		FivIoImage *scaled =
 | 
			
		||||
			adjust_thumbnail(image, fiv_thumbnail_sizes[use].size);
 | 
			
		||||
		gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir,
 | 
			
		||||
			fiv_thumbnail_sizes[use].thumbnail_spec_name, sum);
 | 
			
		||||
		save_thumbnail(scaled, path, thum);
 | 
			
		||||
		g_free(path);
 | 
			
		||||
 | 
			
		||||
		if (!max_size_surface)
 | 
			
		||||
			max_size_surface = scaled;
 | 
			
		||||
		if (!max_size_image)
 | 
			
		||||
			max_size_image = scaled;
 | 
			
		||||
		else
 | 
			
		||||
			cairo_surface_destroy(scaled);
 | 
			
		||||
			fiv_io_image_unref(scaled);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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(sum);
 | 
			
		||||
	g_free(uri);
 | 
			
		||||
	cairo_surface_destroy(surface);
 | 
			
		||||
	return max_size_surface;
 | 
			
		||||
	fiv_io_image_unref(image);
 | 
			
		||||
	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)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	cairo_surface_t *surface =
 | 
			
		||||
	FivIoImage *image =
 | 
			
		||||
		fiv_io_open(&(FivIoOpenContext){.uri = thumbnail_uri}, error);
 | 
			
		||||
	g_free(thumbnail_uri);
 | 
			
		||||
	if (!surface)
 | 
			
		||||
	if (!image)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	bool sRGB = false;
 | 
			
		||||
	GBytes *thum = cairo_surface_get_user_data(surface, &fiv_io_key_thum);
 | 
			
		||||
	if (!thum) {
 | 
			
		||||
	if (!image->thum) {
 | 
			
		||||
		g_clear_error(error);
 | 
			
		||||
		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);
 | 
			
		||||
		set_error(error, "mismatch");
 | 
			
		||||
	} else {
 | 
			
		||||
		// TODO(p): Add a function or a non-valueless define to check
 | 
			
		||||
		// for CMM presence, then remove this ifdef.
 | 
			
		||||
		cairo_surface_t *surface = fiv_io_image_to_surface(image);
 | 
			
		||||
#ifdef HAVE_LCMS2
 | 
			
		||||
		if (!sRGB)
 | 
			
		||||
			mark_thumbnail_lq(surface);
 | 
			
		||||
@@ -815,21 +844,21 @@ read_wide_thumbnail(const char *path, const Stat *st, GError **error)
 | 
			
		||||
		return surface;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cairo_surface_destroy(surface);
 | 
			
		||||
	fiv_io_image_unref(image);
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
read_png_thumbnail(const char *path, const Stat *st, GError **error)
 | 
			
		||||
{
 | 
			
		||||
	cairo_surface_t *surface = fiv_io_open_png_thumbnail(path, error);
 | 
			
		||||
	if (!surface)
 | 
			
		||||
	FivIoImage *image = fiv_io_open_png_thumbnail(path, error);
 | 
			
		||||
	if (!image)
 | 
			
		||||
		return NULL;
 | 
			
		||||
 | 
			
		||||
	GHashTable *texts = cairo_surface_get_user_data(surface, &fiv_io_key_text);
 | 
			
		||||
	GHashTable *texts = image->text;
 | 
			
		||||
	if (!texts) {
 | 
			
		||||
		set_error(error, "not a thumbnail");
 | 
			
		||||
		cairo_surface_destroy(surface);
 | 
			
		||||
		fiv_io_image_unref(image);
 | 
			
		||||
		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) ||
 | 
			
		||||
		!text_mtime || atol(text_mtime) != st->mtime) {
 | 
			
		||||
		set_error(error, "mismatch or not a thumbnail");
 | 
			
		||||
		cairo_surface_destroy(surface);
 | 
			
		||||
		fiv_io_image_unref(image);
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
	if (text_size && strtoull(text_size, NULL, 10) != st->size) {
 | 
			
		||||
		set_error(error, "file size mismatch");
 | 
			
		||||
		cairo_surface_destroy(surface);
 | 
			
		||||
		fiv_io_image_unref(image);
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return surface;
 | 
			
		||||
	return fiv_io_image_to_surface(image);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										628
									
								
								fiv-view.c
									
									
									
									
									
								
							
							
						
						
									
										628
									
								
								fiv-view.c
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
//
 | 
			
		||||
// fiv-view.c: image viewing widget
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -24,6 +24,7 @@
 | 
			
		||||
#include <math.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
 | 
			
		||||
#include <epoxy/gl.h>
 | 
			
		||||
#include <gtk/gtk.h>
 | 
			
		||||
#ifdef GDK_WINDOWING_X11
 | 
			
		||||
#include <gdk/gdkx.h>
 | 
			
		||||
@@ -63,10 +64,10 @@ struct _FivView {
 | 
			
		||||
 | 
			
		||||
	gchar *messages;                    ///< Image load information
 | 
			
		||||
	gchar *uri;                         ///< Path to the current image (if any)
 | 
			
		||||
	cairo_surface_t *image;             ///< The loaded image (sequence)
 | 
			
		||||
	cairo_surface_t *page;              ///< Current page within image, weak
 | 
			
		||||
	cairo_surface_t *page_scaled;       ///< Current page within image, scaled
 | 
			
		||||
	cairo_surface_t *frame;             ///< Current frame within page, weak
 | 
			
		||||
	FivIoImage *image;                  ///< The loaded image (sequence)
 | 
			
		||||
	FivIoImage *page;                   ///< Current page within image, weak
 | 
			
		||||
	FivIoImage *page_scaled;            ///< Current page within image, scaled
 | 
			
		||||
	FivIoImage *frame;                  ///< Current frame within page, weak
 | 
			
		||||
	FivIoOrientation orientation;       ///< Current page orientation
 | 
			
		||||
	bool enable_cms : 1;                ///< Smooth scaling toggle
 | 
			
		||||
	bool filter : 1;                    ///< Smooth scaling toggle
 | 
			
		||||
@@ -77,12 +78,16 @@ struct _FivView {
 | 
			
		||||
	double scale;                       ///< Scaling factor
 | 
			
		||||
	double drag_start[2];               ///< Adjustment values for drag origin
 | 
			
		||||
 | 
			
		||||
	cairo_surface_t *enhance_swap;      ///< Quick swap in/out
 | 
			
		||||
	FivIoProfile screen_cms_profile;    ///< Target colour profile for widget
 | 
			
		||||
	FivIoImage *enhance_swap;           ///< Quick swap in/out
 | 
			
		||||
	FivIoProfile *screen_cms_profile;   ///< Target colour profile for widget
 | 
			
		||||
 | 
			
		||||
	int remaining_loops;                ///< Greater than zero if limited
 | 
			
		||||
	gint64 frame_time;                  ///< Current frame's start, µs precision
 | 
			
		||||
	gulong frame_update_connection;     ///< GdkFrameClock::update
 | 
			
		||||
 | 
			
		||||
	GdkGLContext *gl_context;           ///< OpenGL context
 | 
			
		||||
	bool gl_initialized;                ///< Objects have been created
 | 
			
		||||
	GLuint gl_program;                  ///< Linked render program
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0,
 | 
			
		||||
@@ -161,6 +166,147 @@ enum {
 | 
			
		||||
// Globals are, sadly, the canonical way of storing signal numbers.
 | 
			
		||||
static guint view_signals[LAST_SIGNAL];
 | 
			
		||||
 | 
			
		||||
// --- OpenGL ------------------------------------------------------------------
 | 
			
		||||
// While GTK+ 3 technically still supports legacy desktop OpenGL 2.0[1],
 | 
			
		||||
// we will pick the 3.3 core profile, which is fairly old by now.
 | 
			
		||||
// It doesn't seem to make any sense to go below 3.2.
 | 
			
		||||
//
 | 
			
		||||
// [1] https://stackoverflow.com/a/37923507/76313
 | 
			
		||||
//
 | 
			
		||||
// OpenGL ES
 | 
			
		||||
//
 | 
			
		||||
// Currently, we do not support OpenGL ES at all--it needs its own shaders
 | 
			
		||||
// (if only because of different #version statements), and also further analysis
 | 
			
		||||
// as to what is our minimum version requirement. While GTK+ 3 can again go
 | 
			
		||||
// down as low as OpenGL ES 2.0, this might be too much of a hassle to support.
 | 
			
		||||
//
 | 
			
		||||
// ES can be forced via GDK_GL=gles, if gdk_gl_context_set_required_version()
 | 
			
		||||
// doesn't stand in the way.
 | 
			
		||||
//
 | 
			
		||||
// Let's not forget that this is a desktop image viewer first and foremost.
 | 
			
		||||
 | 
			
		||||
static const char *
 | 
			
		||||
gl_error_string(GLenum err)
 | 
			
		||||
{
 | 
			
		||||
	switch (err) {
 | 
			
		||||
	case GL_NO_ERROR:
 | 
			
		||||
		return "no error";
 | 
			
		||||
	case GL_CONTEXT_LOST:
 | 
			
		||||
		return "context lost";
 | 
			
		||||
	case GL_INVALID_ENUM:
 | 
			
		||||
		return "invalid enum";
 | 
			
		||||
	case GL_INVALID_VALUE:
 | 
			
		||||
		return "invalid value";
 | 
			
		||||
	case GL_INVALID_OPERATION:
 | 
			
		||||
		return "invalid operation";
 | 
			
		||||
	case GL_INVALID_FRAMEBUFFER_OPERATION:
 | 
			
		||||
		return "invalid framebuffer operation";
 | 
			
		||||
	case GL_OUT_OF_MEMORY:
 | 
			
		||||
		return "out of memory";
 | 
			
		||||
	case GL_STACK_UNDERFLOW:
 | 
			
		||||
		return "stack underflow";
 | 
			
		||||
	case GL_STACK_OVERFLOW:
 | 
			
		||||
		return "stack overflow";
 | 
			
		||||
	default:
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
static const char *gl_vertex =
 | 
			
		||||
	"#version 330\n"
 | 
			
		||||
	"layout(location = 0) in vec4 position;\n"
 | 
			
		||||
	"out vec2 coordinates;\n"
 | 
			
		||||
	"void main() {\n"
 | 
			
		||||
	"\tcoordinates = position.zw;\n"
 | 
			
		||||
	"\tgl_Position = vec4(position.xy, 0., 1.);\n"
 | 
			
		||||
	"}\n";
 | 
			
		||||
 | 
			
		||||
static const char *gl_fragment =
 | 
			
		||||
	"#version 330\n"
 | 
			
		||||
	"in vec2 coordinates;\n"
 | 
			
		||||
	"layout(location = 0) out vec4 color;\n"
 | 
			
		||||
	"uniform sampler2D picture;\n"
 | 
			
		||||
	"uniform bool checkerboard;\n"
 | 
			
		||||
	"\n"
 | 
			
		||||
	"vec3 checker() {\n"
 | 
			
		||||
	"\tvec2 xy = gl_FragCoord.xy / 20.;\n"
 | 
			
		||||
	"\tif (checkerboard && (int(floor(xy.x) + floor(xy.y)) & 1) == 0)\n"
 | 
			
		||||
	"\t\treturn vec3(0.98);\n"
 | 
			
		||||
	"\telse\n"
 | 
			
		||||
	"\t\treturn vec3(1.00);\n"
 | 
			
		||||
	"}\n"
 | 
			
		||||
	"\n"
 | 
			
		||||
	"void main() {\n"
 | 
			
		||||
	"\tvec3 c = checker();\n"
 | 
			
		||||
	"\tvec4 t = texture(picture, coordinates);\n"
 | 
			
		||||
	"\t// Premultiplied blending with a solid background.\n"
 | 
			
		||||
	"\t// XXX: This is only correct for linear components.\n"
 | 
			
		||||
	"\tcolor = vec4(c * (1. - t.a) + t.rgb, 1.);\n"
 | 
			
		||||
	"}\n";
 | 
			
		||||
 | 
			
		||||
static GLuint
 | 
			
		||||
gl_make_shader(int type, const char *glsl)
 | 
			
		||||
{
 | 
			
		||||
	GLuint shader = glCreateShader(type);
 | 
			
		||||
	glShaderSource(shader, 1, &glsl, NULL);
 | 
			
		||||
	glCompileShader(shader);
 | 
			
		||||
 | 
			
		||||
	GLint status = 0;
 | 
			
		||||
	glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
 | 
			
		||||
	if (!status) {
 | 
			
		||||
		GLint len = 0;
 | 
			
		||||
		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
 | 
			
		||||
 | 
			
		||||
		GLchar *buffer = g_malloc0(len + 1);
 | 
			
		||||
		glGetShaderInfoLog(shader, len, NULL, buffer);
 | 
			
		||||
		g_warning("GL shader compilation failed: %s", buffer);
 | 
			
		||||
		g_free(buffer);
 | 
			
		||||
 | 
			
		||||
		glDeleteShader(shader);
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
	return shader;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static GLuint
 | 
			
		||||
gl_make_program(void)
 | 
			
		||||
{
 | 
			
		||||
	GLuint vertex = gl_make_shader(GL_VERTEX_SHADER, gl_vertex);
 | 
			
		||||
	GLuint fragment = gl_make_shader(GL_FRAGMENT_SHADER, gl_fragment);
 | 
			
		||||
	if (!vertex || !fragment) {
 | 
			
		||||
		glDeleteShader(vertex);
 | 
			
		||||
		glDeleteShader(fragment);
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	GLuint program = glCreateProgram();
 | 
			
		||||
	glAttachShader(program, vertex);
 | 
			
		||||
	glAttachShader(program, fragment);
 | 
			
		||||
	glLinkProgram(program);
 | 
			
		||||
	glDeleteShader(vertex);
 | 
			
		||||
	glDeleteShader(fragment);
 | 
			
		||||
 | 
			
		||||
	GLint status = 0;
 | 
			
		||||
	glGetProgramiv(program, GL_LINK_STATUS, &status);
 | 
			
		||||
	if (!status) {
 | 
			
		||||
		GLint len = 0;
 | 
			
		||||
		glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
 | 
			
		||||
 | 
			
		||||
		GLchar *buffer = g_malloc0(len + 1);
 | 
			
		||||
		glGetProgramInfoLog(program, len, NULL, buffer);
 | 
			
		||||
		g_warning("GL program linking failed: %s", buffer);
 | 
			
		||||
		g_free(buffer);
 | 
			
		||||
 | 
			
		||||
		glDeleteProgram(program);
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
	return program;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// -----------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
on_adjustment_value_changed(
 | 
			
		||||
	G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
 | 
			
		||||
@@ -198,12 +344,14 @@ update_adjustments(FivView *self)
 | 
			
		||||
 | 
			
		||||
	if (self->hadjustment) {
 | 
			
		||||
		gtk_adjustment_configure(self->hadjustment,
 | 
			
		||||
			gtk_adjustment_get_value(self->hadjustment), 0, dw,
 | 
			
		||||
			gtk_adjustment_get_value(self->hadjustment),
 | 
			
		||||
			0, MAX(dw, alloc.width),
 | 
			
		||||
			alloc.width * 0.1, alloc.width * 0.9, alloc.width);
 | 
			
		||||
	}
 | 
			
		||||
	if (self->vadjustment) {
 | 
			
		||||
		gtk_adjustment_configure(self->vadjustment,
 | 
			
		||||
			gtk_adjustment_get_value(self->vadjustment), 0, dh,
 | 
			
		||||
			gtk_adjustment_get_value(self->vadjustment),
 | 
			
		||||
			0, MAX(dh, alloc.height),
 | 
			
		||||
			alloc.height * 0.1, alloc.height * 0.9, alloc.height);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -234,9 +382,9 @@ fiv_view_finalize(GObject *gobject)
 | 
			
		||||
{
 | 
			
		||||
	FivView *self = FIV_VIEW(gobject);
 | 
			
		||||
	g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free);
 | 
			
		||||
	g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
 | 
			
		||||
	g_clear_pointer(&self->image, cairo_surface_destroy);
 | 
			
		||||
	g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
 | 
			
		||||
	g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
 | 
			
		||||
	g_clear_pointer(&self->image, fiv_io_image_unref);
 | 
			
		||||
	g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
 | 
			
		||||
	g_free(self->uri);
 | 
			
		||||
	g_free(self->messages);
 | 
			
		||||
 | 
			
		||||
@@ -283,15 +431,13 @@ fiv_view_get_property(
 | 
			
		||||
		g_value_set_boolean(value, !!self->image);
 | 
			
		||||
		break;
 | 
			
		||||
	case PROP_CAN_ANIMATE:
 | 
			
		||||
		g_value_set_boolean(value, self->page &&
 | 
			
		||||
			cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next));
 | 
			
		||||
		g_value_set_boolean(value, self->page && self->page->frame_next);
 | 
			
		||||
		break;
 | 
			
		||||
	case PROP_HAS_PREVIOUS_PAGE:
 | 
			
		||||
		g_value_set_boolean(value, self->image && self->page != self->image);
 | 
			
		||||
		break;
 | 
			
		||||
	case PROP_HAS_NEXT_PAGE:
 | 
			
		||||
		g_value_set_boolean(value, self->page &&
 | 
			
		||||
			cairo_surface_get_user_data(self->page, &fiv_io_key_page_next));
 | 
			
		||||
		g_value_set_boolean(value, self->page && self->page->page_next);
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case PROP_HADJUSTMENT:
 | 
			
		||||
@@ -403,20 +549,34 @@ static void
 | 
			
		||||
prescale_page(FivView *self)
 | 
			
		||||
{
 | 
			
		||||
	FivIoRenderClosure *closure = NULL;
 | 
			
		||||
	if (!self->image || !(closure =
 | 
			
		||||
			cairo_surface_get_user_data(self->page, &fiv_io_key_render)))
 | 
			
		||||
	if (!self->image || !(closure = self->page->render))
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Restart the animation. No vector formats currently animate.
 | 
			
		||||
	g_return_if_fail(!self->frame_update_connection);
 | 
			
		||||
 | 
			
		||||
	// Optimization, taking into account the workaround in set_scale().
 | 
			
		||||
	if (!self->page_scaled &&
 | 
			
		||||
		(self->scale == 1 || self->scale == 0.999999999999999))
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	// If it fails, the previous frame pointer may become invalid.
 | 
			
		||||
	g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
 | 
			
		||||
	self->frame = self->page_scaled = closure->render(closure, self->scale);
 | 
			
		||||
	g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
 | 
			
		||||
	self->frame = self->page_scaled = closure->render(closure,
 | 
			
		||||
		self->enable_cms ? fiv_io_cmm_get_default() : NULL,
 | 
			
		||||
		self->enable_cms ? self->screen_cms_profile : NULL, self->scale);
 | 
			
		||||
	if (!self->page_scaled)
 | 
			
		||||
		self->frame = self->page;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
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:
 | 
			
		||||
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
 | 
			
		||||
static FivIoProfile *
 | 
			
		||||
monitor_cms_profile(GdkWindow *root, int num)
 | 
			
		||||
{
 | 
			
		||||
	char atom[32] = "";
 | 
			
		||||
	g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num);
 | 
			
		||||
 | 
			
		||||
	// Sadly, there is no nice GTK+/GDK mechanism to watch this for changes.
 | 
			
		||||
	int format = 0, length = 0;
 | 
			
		||||
	GdkAtom type = GDK_NONE;
 | 
			
		||||
	guchar *data = NULL;
 | 
			
		||||
	FivIoProfile *result = NULL;
 | 
			
		||||
	if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
 | 
			
		||||
			8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
 | 
			
		||||
		if (format == 8 && length > 0)
 | 
			
		||||
			result = fiv_io_cmm_get_profile(
 | 
			
		||||
				fiv_io_cmm_get_default(), data, length);
 | 
			
		||||
		g_free(data);
 | 
			
		||||
	}
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
reload_screen_cms_profile(FivView *self, GdkWindow *window)
 | 
			
		||||
{
 | 
			
		||||
@@ -465,7 +646,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
 | 
			
		||||
				gchar *data = NULL;
 | 
			
		||||
				gsize length = 0;
 | 
			
		||||
				if (g_file_get_contents(path, &data, &length, NULL))
 | 
			
		||||
					self->screen_cms_profile = fiv_io_profile_new(data, length);
 | 
			
		||||
					self->screen_cms_profile = fiv_io_cmm_get_profile(
 | 
			
		||||
						fiv_io_cmm_get_default(), data, length);
 | 
			
		||||
				g_free(data);
 | 
			
		||||
			}
 | 
			
		||||
			g_free(path);
 | 
			
		||||
@@ -477,6 +659,7 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
 | 
			
		||||
 | 
			
		||||
	GdkDisplay *display = gdk_window_get_display(window);
 | 
			
		||||
	GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
 | 
			
		||||
	GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
 | 
			
		||||
 | 
			
		||||
	int num = -1;
 | 
			
		||||
	for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; )
 | 
			
		||||
@@ -485,24 +668,14 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
 | 
			
		||||
	if (num < 0)
 | 
			
		||||
		goto out;
 | 
			
		||||
 | 
			
		||||
	char atom[32] = "";
 | 
			
		||||
	g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num);
 | 
			
		||||
 | 
			
		||||
	// Sadly, there is no nice GTK+/GDK mechanism to watch this for changes.
 | 
			
		||||
	int format = 0, length = 0;
 | 
			
		||||
	GdkAtom type = GDK_NONE;
 | 
			
		||||
	guchar *data = NULL;
 | 
			
		||||
	GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
 | 
			
		||||
	if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
 | 
			
		||||
			8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
 | 
			
		||||
		if (format == 8 && length > 0)
 | 
			
		||||
			self->screen_cms_profile = fiv_io_profile_new(data, length);
 | 
			
		||||
		g_free(data);
 | 
			
		||||
	}
 | 
			
		||||
	// Cater to xiccd limitations (agalakhov/xiccd#33).
 | 
			
		||||
	if (!(self->screen_cms_profile = monitor_cms_profile(root, num)) && num)
 | 
			
		||||
		self->screen_cms_profile = monitor_cms_profile(root, 0);
 | 
			
		||||
 | 
			
		||||
out:
 | 
			
		||||
	if (!self->screen_cms_profile)
 | 
			
		||||
		self->screen_cms_profile = fiv_io_profile_new_sRGB();
 | 
			
		||||
		self->screen_cms_profile =
 | 
			
		||||
			fiv_io_cmm_get_profile_sRGB(fiv_io_cmm_get_default());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
@@ -536,6 +709,9 @@ fiv_view_realize(GtkWidget *widget)
 | 
			
		||||
	GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
 | 
			
		||||
		&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
 | 
			
		||||
 | 
			
		||||
	GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
 | 
			
		||||
	gboolean opengl = g_settings_get_boolean(settings, "opengl");
 | 
			
		||||
 | 
			
		||||
	// Without the following call, or the rendering mode set to "recording",
 | 
			
		||||
	// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
 | 
			
		||||
	// creates backing stores using cairo_content_t constants.
 | 
			
		||||
@@ -545,19 +721,274 @@ fiv_view_realize(GtkWidget *widget)
 | 
			
		||||
	// Note that this disables double buffering, and sometimes causes artefacts,
 | 
			
		||||
	// see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
 | 
			
		||||
	//
 | 
			
		||||
	// If GTK+'s OpenGL integration fails to deliver, we need to use the window
 | 
			
		||||
	// directly, sidestepping the toolkit entirely.
 | 
			
		||||
	GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
 | 
			
		||||
	// GTK+'s OpenGL integration is terrible, so we may need to use
 | 
			
		||||
	// the X11 subwindow directly, sidestepping the toolkit entirely.
 | 
			
		||||
	if (GDK_IS_X11_WINDOW(window) &&
 | 
			
		||||
		g_settings_get_boolean(settings, "native-view-window"))
 | 
			
		||||
		gdk_window_ensure_native(window);
 | 
			
		||||
#endif  // GDK_WINDOWING_X11
 | 
			
		||||
	g_object_unref(settings);
 | 
			
		||||
 | 
			
		||||
	gtk_widget_register_window(widget, window);
 | 
			
		||||
	gtk_widget_set_window(widget, window);
 | 
			
		||||
	gtk_widget_set_realized(widget, TRUE);
 | 
			
		||||
 | 
			
		||||
	reload_screen_cms_profile(FIV_VIEW(widget), window);
 | 
			
		||||
 | 
			
		||||
	FivView *self = FIV_VIEW(widget);
 | 
			
		||||
	g_clear_object(&self->gl_context);
 | 
			
		||||
	if (!opengl)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	GdkGLContext *gl_context = gdk_window_create_gl_context(window, &error);
 | 
			
		||||
	if (!gl_context) {
 | 
			
		||||
		g_warning("GL: %s", error->message);
 | 
			
		||||
		g_error_free(error);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gdk_gl_context_set_use_es(gl_context, FALSE);
 | 
			
		||||
	gdk_gl_context_set_required_version(gl_context, 3, 3);
 | 
			
		||||
	gdk_gl_context_set_debug_enabled(gl_context, TRUE);
 | 
			
		||||
 | 
			
		||||
	if (!gdk_gl_context_realize(gl_context, &error)) {
 | 
			
		||||
		g_warning("GL: %s", error->message);
 | 
			
		||||
		g_error_free(error);
 | 
			
		||||
		g_object_unref(gl_context);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	self->gl_context = gl_context;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void GLAPIENTRY
 | 
			
		||||
gl_on_message(G_GNUC_UNUSED GLenum source, GLenum type, G_GNUC_UNUSED GLuint id,
 | 
			
		||||
	G_GNUC_UNUSED GLenum severity, G_GNUC_UNUSED GLsizei length,
 | 
			
		||||
	const GLchar *message, G_GNUC_UNUSED const void *user_data)
 | 
			
		||||
{
 | 
			
		||||
	if (type == GL_DEBUG_TYPE_ERROR)
 | 
			
		||||
		g_warning("GL: error: %s", message);
 | 
			
		||||
	else
 | 
			
		||||
		g_debug("GL: %s", message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
fiv_view_unrealize(GtkWidget *widget)
 | 
			
		||||
{
 | 
			
		||||
	FivView *self = FIV_VIEW(widget);
 | 
			
		||||
	if (self->gl_context) {
 | 
			
		||||
		if (self->gl_initialized) {
 | 
			
		||||
			gdk_gl_context_make_current(self->gl_context);
 | 
			
		||||
			glDeleteProgram(self->gl_program);
 | 
			
		||||
		}
 | 
			
		||||
		if (self->gl_context == gdk_gl_context_get_current())
 | 
			
		||||
			gdk_gl_context_clear_current();
 | 
			
		||||
 | 
			
		||||
		g_clear_object(&self->gl_context);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	GTK_WIDGET_CLASS(fiv_view_parent_class)->unrealize(widget);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool
 | 
			
		||||
gl_draw(FivView *self, cairo_t *cr)
 | 
			
		||||
{
 | 
			
		||||
	gdk_gl_context_make_current(self->gl_context);
 | 
			
		||||
 | 
			
		||||
	if (!self->gl_initialized) {
 | 
			
		||||
		GLuint program = gl_make_program();
 | 
			
		||||
		if (!program)
 | 
			
		||||
			return false;
 | 
			
		||||
 | 
			
		||||
		glDisable(GL_SCISSOR_TEST);
 | 
			
		||||
		glDisable(GL_STENCIL_TEST);
 | 
			
		||||
		glDisable(GL_DEPTH_TEST);
 | 
			
		||||
		glDisable(GL_CULL_FACE);
 | 
			
		||||
		glDisable(GL_BLEND);
 | 
			
		||||
		if (epoxy_has_gl_extension("GL_ARB_debug_output")) {
 | 
			
		||||
			glEnable(GL_DEBUG_OUTPUT);
 | 
			
		||||
			glDebugMessageCallback(gl_on_message, NULL);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		self->gl_program = program;
 | 
			
		||||
		self->gl_initialized = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// This limit is always less than that of Cairo/pixman,
 | 
			
		||||
	// and we'd have to figure out tiling.
 | 
			
		||||
	GLint max = 0;
 | 
			
		||||
	glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
 | 
			
		||||
	if (max < (GLint) self->frame->width ||
 | 
			
		||||
		max < (GLint) self->frame->height) {
 | 
			
		||||
		g_warning("OpenGL max. texture size is too small");
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	GtkAllocation allocation;
 | 
			
		||||
	gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
 | 
			
		||||
	int dw = 0, dh = 0, dx = 0, dy = 0;
 | 
			
		||||
	get_display_dimensions(self, &dw, &dh);
 | 
			
		||||
 | 
			
		||||
	int clipw = dw, cliph = dh;
 | 
			
		||||
	double x1 = 0., y1 = 0., x2 = 1., y2 = 1.;
 | 
			
		||||
	if (self->hadjustment)
 | 
			
		||||
		x1 = floor(gtk_adjustment_get_value(self->hadjustment)) / dw;
 | 
			
		||||
	if (self->vadjustment)
 | 
			
		||||
		y1 = floor(gtk_adjustment_get_value(self->vadjustment)) / dh;
 | 
			
		||||
 | 
			
		||||
	if (dw <= allocation.width) {
 | 
			
		||||
		dx = round((allocation.width - dw) / 2.);
 | 
			
		||||
	} else {
 | 
			
		||||
		x2 = x1 + (double) allocation.width / dw;
 | 
			
		||||
		clipw = allocation.width;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (dh <= allocation.height) {
 | 
			
		||||
		dy = round((allocation.height - dh) / 2.);
 | 
			
		||||
	} else {
 | 
			
		||||
		y2 = y1 + (double) allocation.height / dh;
 | 
			
		||||
		cliph = allocation.height;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self));
 | 
			
		||||
	clipw *= scale;
 | 
			
		||||
	cliph *= scale;
 | 
			
		||||
 | 
			
		||||
	enum { SRC, DEST };
 | 
			
		||||
	GLuint textures[2] = {};
 | 
			
		||||
	glGenTextures(2, textures);
 | 
			
		||||
 | 
			
		||||
	// https://stackoverflow.com/questions/25157306 0..1
 | 
			
		||||
	// GL_TEXTURE_RECTANGLE seems kind-of useful
 | 
			
		||||
	glBindTexture(GL_TEXTURE_2D, textures[SRC]);
 | 
			
		||||
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 | 
			
		||||
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 | 
			
		||||
	if (self->filter) {
 | 
			
		||||
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 | 
			
		||||
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 | 
			
		||||
	} else {
 | 
			
		||||
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 | 
			
		||||
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// GL_UNPACK_ALIGNMENT is initially 4, which is fine for these.
 | 
			
		||||
	// Texture swizzling is OpenGL 3.3.
 | 
			
		||||
	if (self->frame->format == CAIRO_FORMAT_ARGB32) {
 | 
			
		||||
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
 | 
			
		||||
			self->frame->width, self->frame->height,
 | 
			
		||||
			0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
 | 
			
		||||
	} else if (self->frame->format == CAIRO_FORMAT_RGB24) {
 | 
			
		||||
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
 | 
			
		||||
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
 | 
			
		||||
			self->frame->width, self->frame->height,
 | 
			
		||||
			0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
 | 
			
		||||
	} else if (self->frame->format == CAIRO_FORMAT_RGB30) {
 | 
			
		||||
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
 | 
			
		||||
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
 | 
			
		||||
			self->frame->width, self->frame->height,
 | 
			
		||||
			0, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV, self->frame->data);
 | 
			
		||||
	} else {
 | 
			
		||||
		g_warning("GL: unsupported bitmap format");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// GtkGLArea creates textures like this.
 | 
			
		||||
	glBindTexture(GL_TEXTURE_2D, textures[DEST]);
 | 
			
		||||
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 | 
			
		||||
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 | 
			
		||||
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, clipw, cliph, 0, GL_BGRA,
 | 
			
		||||
		GL_UNSIGNED_BYTE, NULL);
 | 
			
		||||
 | 
			
		||||
	glViewport(0, 0, clipw, cliph);
 | 
			
		||||
 | 
			
		||||
	GLuint vao = 0;
 | 
			
		||||
	glGenVertexArrays(1, &vao);
 | 
			
		||||
 | 
			
		||||
	GLuint frame_buffer = 0;
 | 
			
		||||
	glGenFramebuffers(1, &frame_buffer);
 | 
			
		||||
	glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);
 | 
			
		||||
	glFramebufferTexture2D(
 | 
			
		||||
		GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[DEST], 0);
 | 
			
		||||
 | 
			
		||||
	glClearColor(0., 0., 0., 1.);
 | 
			
		||||
	glClear(GL_COLOR_BUFFER_BIT);
 | 
			
		||||
 | 
			
		||||
	GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
 | 
			
		||||
	if (status != GL_FRAMEBUFFER_COMPLETE)
 | 
			
		||||
		g_warning("GL framebuffer status: %u", status);
 | 
			
		||||
 | 
			
		||||
	glUseProgram(self->gl_program);
 | 
			
		||||
	GLint position_location = glGetAttribLocation(
 | 
			
		||||
		self->gl_program, "position");
 | 
			
		||||
	GLint picture_location = glGetUniformLocation(
 | 
			
		||||
		self->gl_program, "picture");
 | 
			
		||||
	GLint checkerboard_location = glGetUniformLocation(
 | 
			
		||||
		self->gl_program, "checkerboard");
 | 
			
		||||
 | 
			
		||||
	glUniform1i(picture_location, 0);
 | 
			
		||||
	glUniform1i(checkerboard_location, self->checkerboard);
 | 
			
		||||
	glActiveTexture(GL_TEXTURE0);
 | 
			
		||||
	glBindTexture(GL_TEXTURE_2D, textures[SRC]);
 | 
			
		||||
 | 
			
		||||
	// Note that the Y axis is flipped in the table.
 | 
			
		||||
	double vertices[][4] = {
 | 
			
		||||
		{-1., -1., x1, y2},
 | 
			
		||||
		{+1., -1., x2, y2},
 | 
			
		||||
		{+1., +1., x2, y1},
 | 
			
		||||
		{-1., +1., x1, y1},
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	cairo_matrix_t matrix = fiv_io_orientation_matrix(self->orientation, 1, 1);
 | 
			
		||||
	cairo_matrix_transform_point(&matrix, &vertices[0][2], &vertices[0][3]);
 | 
			
		||||
	cairo_matrix_transform_point(&matrix, &vertices[1][2], &vertices[1][3]);
 | 
			
		||||
	cairo_matrix_transform_point(&matrix, &vertices[2][2], &vertices[2][3]);
 | 
			
		||||
	cairo_matrix_transform_point(&matrix, &vertices[3][2], &vertices[3][3]);
 | 
			
		||||
 | 
			
		||||
	GLuint vertex_buffer = 0;
 | 
			
		||||
	glGenBuffers(1, &vertex_buffer);
 | 
			
		||||
	glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
 | 
			
		||||
	glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
 | 
			
		||||
	glBindVertexArray(vao);
 | 
			
		||||
	glVertexAttribPointer(position_location,
 | 
			
		||||
		G_N_ELEMENTS(vertices[0]), GL_DOUBLE, GL_FALSE, sizeof vertices[0], 0);
 | 
			
		||||
	glEnableVertexAttribArray(position_location);
 | 
			
		||||
	glDrawArrays(GL_TRIANGLE_FAN, 0, G_N_ELEMENTS(vertices));
 | 
			
		||||
	glDisableVertexAttribArray(position_location);
 | 
			
		||||
	glBindVertexArray(0);
 | 
			
		||||
	glBindBuffer(GL_ARRAY_BUFFER, 0);
 | 
			
		||||
	glUseProgram(0);
 | 
			
		||||
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
 | 
			
		||||
 | 
			
		||||
	// XXX: Native GdkWindows send this to the software fallback path.
 | 
			
		||||
	// XXX: This only reliably alpha blends when using the software fallback,
 | 
			
		||||
	// such as with a native window, because 7237f5d in GTK+ 3 is a regression.
 | 
			
		||||
	// (Introduced in 3.24.39, reverted in 3.24.42.)
 | 
			
		||||
	//
 | 
			
		||||
	// We had to resort to rendering the checkerboard pattern in the shader.
 | 
			
		||||
	// Unfortunately, it is hard to retrieve the theme colours from CSS.
 | 
			
		||||
	GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
 | 
			
		||||
	cairo_translate(cr, dx, dy);
 | 
			
		||||
	gdk_cairo_draw_from_gl(
 | 
			
		||||
		cr, window, textures[DEST], GL_TEXTURE, scale, 0, 0, clipw, cliph);
 | 
			
		||||
	gdk_gl_context_make_current(self->gl_context);
 | 
			
		||||
 | 
			
		||||
	glDeleteBuffers(1, &vertex_buffer);
 | 
			
		||||
	glDeleteTextures(2, textures);
 | 
			
		||||
	glDeleteVertexArrays(1, &vao);
 | 
			
		||||
	glDeleteFramebuffers(1, &frame_buffer);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Possibly use this clue as a hint to use Cairo rendering.
 | 
			
		||||
	GLenum err = 0;
 | 
			
		||||
	while ((err = glGetError()) != GL_NO_ERROR) {
 | 
			
		||||
		const char *string = gl_error_string(err);
 | 
			
		||||
		if (string)
 | 
			
		||||
			g_warning("GL: error: %s", string);
 | 
			
		||||
		else
 | 
			
		||||
			g_warning("GL: error: %u", err);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gdk_gl_context_clear_current();
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static gboolean
 | 
			
		||||
@@ -574,8 +1005,10 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
 | 
			
		||||
	if (!self->image ||
 | 
			
		||||
		!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
 | 
			
		||||
		return TRUE;
 | 
			
		||||
	if (self->gl_context && gl_draw(self, cr))
 | 
			
		||||
		return TRUE;
 | 
			
		||||
 | 
			
		||||
	int dw, dh;
 | 
			
		||||
	int dw = 0, dh = 0;
 | 
			
		||||
	get_display_dimensions(self, &dw, &dh);
 | 
			
		||||
 | 
			
		||||
	double x = 0;
 | 
			
		||||
@@ -606,37 +1039,19 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
 | 
			
		||||
 | 
			
		||||
	// Then all frames are pre-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_paint(cr);
 | 
			
		||||
		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
 | 
			
		||||
	// a pixel's worth of made-up picture data.
 | 
			
		||||
	cairo_rectangle(cr, 0, 0, dw, dh);
 | 
			
		||||
	cairo_clip(cr);
 | 
			
		||||
 | 
			
		||||
	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_set_matrix(pattern, &matrix);
 | 
			
		||||
@@ -810,15 +1225,13 @@ stop_animating(FivView *self)
 | 
			
		||||
 | 
			
		||||
	self->frame_time = 0;
 | 
			
		||||
	self->frame_update_connection = 0;
 | 
			
		||||
	self->remaining_loops = 0;
 | 
			
		||||
	g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static gboolean
 | 
			
		||||
advance_frame(FivView *self)
 | 
			
		||||
{
 | 
			
		||||
	cairo_surface_t *next =
 | 
			
		||||
		cairo_surface_get_user_data(self->frame, &fiv_io_key_frame_next);
 | 
			
		||||
	FivIoImage *next = self->frame->frame_next;
 | 
			
		||||
	if (next) {
 | 
			
		||||
		self->frame = next;
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -836,8 +1249,7 @@ advance_animation(FivView *self, GdkFrameClock *clock)
 | 
			
		||||
	gint64 now = gdk_frame_clock_get_frame_time(clock);
 | 
			
		||||
	while (true) {
 | 
			
		||||
		// TODO(p): See if infinite frames can actually happen, and how.
 | 
			
		||||
		intptr_t duration = (intptr_t) cairo_surface_get_user_data(
 | 
			
		||||
			self->frame, &fiv_io_key_frame_duration);
 | 
			
		||||
		int64_t duration = self->frame->frame_duration;
 | 
			
		||||
		if (duration < 0)
 | 
			
		||||
			return FALSE;
 | 
			
		||||
 | 
			
		||||
@@ -875,32 +1287,43 @@ start_animating(FivView *self)
 | 
			
		||||
	stop_animating(self);
 | 
			
		||||
 | 
			
		||||
	GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self));
 | 
			
		||||
	if (!clock || !self->image ||
 | 
			
		||||
		!cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next))
 | 
			
		||||
	if (!clock || !self->image || !self->page->frame_next)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	self->frame_time = gdk_frame_clock_get_frame_time(clock);
 | 
			
		||||
	self->frame_update_connection = g_signal_connect(
 | 
			
		||||
		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);
 | 
			
		||||
	g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
	// XXX: When self->scale_to_fit is in effect,
 | 
			
		||||
	// this uses an old value that may no longer be appropriate,
 | 
			
		||||
	// resulting in wasted effort.
 | 
			
		||||
	prescale_page(self);
 | 
			
		||||
 | 
			
		||||
	if (!self->page ||
 | 
			
		||||
		(self->orientation = (uintptr_t) cairo_surface_get_user_data(
 | 
			
		||||
			 self->page, &fiv_io_key_orientation)) == FivIoOrientationUnknown)
 | 
			
		||||
		(self->orientation = self->page->orientation) ==
 | 
			
		||||
			FivIoOrientationUnknown)
 | 
			
		||||
		self->orientation = FivIoOrientation0;
 | 
			
		||||
 | 
			
		||||
	self->remaining_loops = 0;
 | 
			
		||||
	start_animating(self);
 | 
			
		||||
	gtk_widget_queue_resize(GTK_WIDGET(self));
 | 
			
		||||
 | 
			
		||||
@@ -1027,7 +1450,7 @@ copy(FivView *self)
 | 
			
		||||
	cairo_surface_t *transformed =
 | 
			
		||||
		cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
 | 
			
		||||
	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_paint(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_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_paint(cr);
 | 
			
		||||
}
 | 
			
		||||
@@ -1100,10 +1523,10 @@ print(FivView *self)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static gboolean
 | 
			
		||||
save_as(FivView *self, cairo_surface_t *frame)
 | 
			
		||||
save_as(FivView *self, FivIoImage *frame)
 | 
			
		||||
{
 | 
			
		||||
	GtkWindow *window = get_toplevel(GTK_WIDGET(self));
 | 
			
		||||
	FivIoProfile target = NULL;
 | 
			
		||||
	FivIoProfile *target = NULL;
 | 
			
		||||
	if (self->enable_cms && (target = self->screen_cms_profile)) {
 | 
			
		||||
		GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
 | 
			
		||||
			GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
 | 
			
		||||
@@ -1279,6 +1702,7 @@ fiv_view_class_init(FivViewClass *klass)
 | 
			
		||||
	widget_class->map = fiv_view_map;
 | 
			
		||||
	widget_class->unmap = fiv_view_unmap;
 | 
			
		||||
	widget_class->realize = fiv_view_realize;
 | 
			
		||||
	widget_class->unrealize = fiv_view_unrealize;
 | 
			
		||||
	widget_class->draw = fiv_view_draw;
 | 
			
		||||
	widget_class->button_press_event = fiv_view_button_press_event;
 | 
			
		||||
	widget_class->scroll_event = fiv_view_scroll_event;
 | 
			
		||||
@@ -1362,11 +1786,12 @@ fiv_view_init(FivView *self)
 | 
			
		||||
 | 
			
		||||
// --- Public interface --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
static FivIoImage *
 | 
			
		||||
open_without_swapping_in(FivView *self, const char *uri)
 | 
			
		||||
{
 | 
			
		||||
	FivIoOpenContext ctx = {
 | 
			
		||||
		.uri = uri,
 | 
			
		||||
		.cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL,
 | 
			
		||||
		.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
 | 
			
		||||
		.screen_dpi = 96,  // TODO(p): Try to retrieve it from the screen.
 | 
			
		||||
		.enhance = self->enhance,
 | 
			
		||||
@@ -1374,7 +1799,7 @@ open_without_swapping_in(FivView *self, const char *uri)
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	cairo_surface_t *surface = fiv_io_open(&ctx, &error);
 | 
			
		||||
	FivIoImage *image = fiv_io_open(&ctx, &error);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		g_ptr_array_add(ctx.warnings, g_strdup(error->message));
 | 
			
		||||
		g_error_free(error);
 | 
			
		||||
@@ -1387,7 +1812,7 @@ open_without_swapping_in(FivView *self, const char *uri)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	g_ptr_array_free(ctx.warnings, TRUE);
 | 
			
		||||
	return surface;
 | 
			
		||||
	return image;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO(p): Progressive picture loading, or at least async/cancellable.
 | 
			
		||||
@@ -1395,18 +1820,18 @@ gboolean
 | 
			
		||||
fiv_view_set_uri(FivView *self, const char *uri)
 | 
			
		||||
{
 | 
			
		||||
	// 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) {
 | 
			
		||||
		self->enhance = FALSE;
 | 
			
		||||
		g_object_notify_by_pspec(
 | 
			
		||||
			G_OBJECT(self), view_properties[PROP_ENHANCE]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cairo_surface_t *surface = open_without_swapping_in(self, uri);
 | 
			
		||||
	g_clear_pointer(&self->image, cairo_surface_destroy);
 | 
			
		||||
	FivIoImage *image = open_without_swapping_in(self, uri);
 | 
			
		||||
	g_clear_pointer(&self->image, fiv_io_image_unref);
 | 
			
		||||
 | 
			
		||||
	self->frame = self->page = NULL;
 | 
			
		||||
	self->image = surface;
 | 
			
		||||
	self->image = image;
 | 
			
		||||
	switch_page(self, self->image);
 | 
			
		||||
 | 
			
		||||
	// 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_HAS_IMAGE]);
 | 
			
		||||
	return surface != NULL;
 | 
			
		||||
	return image != NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
page_step(FivView *self, int step)
 | 
			
		||||
{
 | 
			
		||||
	cairo_user_data_key_t *key =
 | 
			
		||||
		step < 0 ? &fiv_io_key_page_previous : &fiv_io_key_page_next;
 | 
			
		||||
	cairo_surface_t *page = cairo_surface_get_user_data(self->page, key);
 | 
			
		||||
	FivIoImage *page = step < 0
 | 
			
		||||
		? self->page->page_previous
 | 
			
		||||
		: self->page->page_next;
 | 
			
		||||
	if (page)
 | 
			
		||||
		switch_page(self, page);
 | 
			
		||||
}
 | 
			
		||||
@@ -1435,31 +1860,35 @@ static void
 | 
			
		||||
frame_step(FivView *self, int step)
 | 
			
		||||
{
 | 
			
		||||
	stop_animating(self);
 | 
			
		||||
	cairo_user_data_key_t *key =
 | 
			
		||||
		step < 0 ? &fiv_io_key_frame_previous : &fiv_io_key_frame_next;
 | 
			
		||||
	if (!step || !(self->frame = cairo_surface_get_user_data(self->frame, key)))
 | 
			
		||||
 | 
			
		||||
	if (step > 0) {
 | 
			
		||||
		// 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->remaining_loops = 0;
 | 
			
		||||
	}
 | 
			
		||||
	gtk_widget_queue_draw(GTK_WIDGET(self));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static gboolean
 | 
			
		||||
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]);
 | 
			
		||||
	if (!surface)
 | 
			
		||||
	if (!image)
 | 
			
		||||
		return FALSE;
 | 
			
		||||
 | 
			
		||||
	g_clear_pointer(&self->image, cairo_surface_destroy);
 | 
			
		||||
	g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
 | 
			
		||||
	switch_page(self, (self->image = surface));
 | 
			
		||||
	g_clear_pointer(&self->image, fiv_io_image_unref);
 | 
			
		||||
	g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
 | 
			
		||||
	switch_page(self, (self->image = image));
 | 
			
		||||
	return TRUE;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
swap_enhanced_image(FivView *self)
 | 
			
		||||
{
 | 
			
		||||
	cairo_surface_t *saved = self->image;
 | 
			
		||||
	FivIoImage *saved = self->image;
 | 
			
		||||
	self->image = self->page = self->frame = NULL;
 | 
			
		||||
 | 
			
		||||
	if (self->enhance_swap) {
 | 
			
		||||
@@ -1546,9 +1975,8 @@ fiv_view_command(FivView *self, FivViewCommand command)
 | 
			
		||||
	break; case FIV_VIEW_COMMAND_PAGE_NEXT:
 | 
			
		||||
		page_step(self, +1);
 | 
			
		||||
	break; case FIV_VIEW_COMMAND_PAGE_LAST:
 | 
			
		||||
		for (cairo_surface_t *s = self->page;
 | 
			
		||||
			 (s = cairo_surface_get_user_data(s, &fiv_io_key_page_next)); )
 | 
			
		||||
			self->page = s;
 | 
			
		||||
		for (FivIoImage *I = self->page; (I = I->page_next); )
 | 
			
		||||
			self->page = I;
 | 
			
		||||
		switch_page(self, self->page);
 | 
			
		||||
 | 
			
		||||
	break; case FIV_VIEW_COMMAND_FRAME_FIRST:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										474
									
								
								fiv.c
									
									
									
									
									
								
							
							
						
						
									
										474
									
								
								fiv.c
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
//
 | 
			
		||||
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -27,6 +27,7 @@
 | 
			
		||||
#include <stdarg.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
 | 
			
		||||
#ifdef G_OS_WIN32
 | 
			
		||||
#include <io.h>
 | 
			
		||||
@@ -43,11 +44,6 @@
 | 
			
		||||
#include "fiv-thumbnail.h"
 | 
			
		||||
#include "fiv-view.h"
 | 
			
		||||
 | 
			
		||||
#ifdef HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
#include <lcms2.h>
 | 
			
		||||
#include <lcms2_fast_float.h>
 | 
			
		||||
#endif  // HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
 | 
			
		||||
// --- Utilities ---------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
 | 
			
		||||
@@ -100,16 +96,16 @@ struct key_section {
 | 
			
		||||
static struct key help_keys_general[] = {
 | 
			
		||||
	{"F1", "Show help"},
 | 
			
		||||
	{"F10", "Open menu"},
 | 
			
		||||
	{"<Control>comma", "Preferences"},
 | 
			
		||||
	{"<Control>question", "Keyboard shortcuts"},
 | 
			
		||||
	{"q <Control>q", "Quit"},
 | 
			
		||||
	{"<Control>w", "Quit"},
 | 
			
		||||
	{"<Primary>comma", "Preferences"},
 | 
			
		||||
	{"<Primary>question", "Keyboard shortcuts"},
 | 
			
		||||
	{"q <Primary>q", "Quit"},
 | 
			
		||||
	{"<Primary>w", "Quit"},
 | 
			
		||||
	{}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static struct key help_keys_navigation[] = {
 | 
			
		||||
	{"<Control>l", "Open location..."},
 | 
			
		||||
	{"<Control>n", "Open a new window"},
 | 
			
		||||
	{"<Primary>l", "Open location..."},
 | 
			
		||||
	{"<Primary>n", "Open a new window"},
 | 
			
		||||
	{"<Alt>Left", "Go back in history"},
 | 
			
		||||
	{"<Alt>Right", "Go forward in history"},
 | 
			
		||||
	{}
 | 
			
		||||
@@ -458,7 +454,7 @@ show_about_dialog(GtkWidget *parent)
 | 
			
		||||
 | 
			
		||||
	GtkWidget *website = gtk_label_new(NULL);
 | 
			
		||||
	gtk_label_set_selectable(GTK_LABEL(website), TRUE);
 | 
			
		||||
	const char *url = "https://git.janouch.name/p/" PROJECT_NAME;
 | 
			
		||||
	const char *url = PROJECT_URL;
 | 
			
		||||
	gchar *link = g_strdup_printf("<a href='%s'>%s</a>", url, url);
 | 
			
		||||
	gtk_label_set_markup(GTK_LABEL(website), link);
 | 
			
		||||
	g_free(link);
 | 
			
		||||
@@ -514,6 +510,113 @@ show_about_dialog(GtkWidget *parent)
 | 
			
		||||
	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 --------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
// 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
toggle_sunlight(void)
 | 
			
		||||
{
 | 
			
		||||
@@ -1368,89 +1457,17 @@ toggle_sunlight(void)
 | 
			
		||||
	g_object_set(settings, property, !set, NULL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Cursor keys, e.g., simply cannot be bound through accelerators
 | 
			
		||||
// (and GtkWidget::keynav-failed would arguably be an awful solution).
 | 
			
		||||
//
 | 
			
		||||
// GtkBindingSets can be added directly through GtkStyleContext,
 | 
			
		||||
// but that would still require setting up action signals on the widget class,
 | 
			
		||||
// which is extremely cumbersome.  GtkWidget::move-focus has no return value,
 | 
			
		||||
// so we can't override that and abort further handling.
 | 
			
		||||
//
 | 
			
		||||
// Therefore, bind directly to keypresses.  Order can be fine-tuned with
 | 
			
		||||
// g_signal_connect{,after}(), or overriding the handler and either tactically
 | 
			
		||||
// chaining up or using gtk_window_propagate_key_event().
 | 
			
		||||
static gboolean
 | 
			
		||||
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
 | 
			
		||||
	G_GNUC_UNUSED gpointer data)
 | 
			
		||||
{
 | 
			
		||||
	switch (event->state & gtk_accelerator_get_default_mod_mask()) {
 | 
			
		||||
	case GDK_MOD1_MASK | GDK_SHIFT_MASK:
 | 
			
		||||
		if (event->keyval == GDK_KEY_D)
 | 
			
		||||
			toggle_sunlight();
 | 
			
		||||
		break;
 | 
			
		||||
	case GDK_CONTROL_MASK:
 | 
			
		||||
	case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
 | 
			
		||||
		switch (event->keyval) {
 | 
			
		||||
		case GDK_KEY_h:
 | 
			
		||||
			// XXX: Command-H is already occupied on macOS.
 | 
			
		||||
			gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
 | 
			
		||||
			return TRUE;
 | 
			
		||||
		case GDK_KEY_l:
 | 
			
		||||
			fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
 | 
			
		||||
			return TRUE;
 | 
			
		||||
		case GDK_KEY_n:
 | 
			
		||||
			if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
 | 
			
		||||
				spawn_uri(g.uri);
 | 
			
		||||
			else
 | 
			
		||||
				spawn_uri(g.directory);
 | 
			
		||||
			return TRUE;
 | 
			
		||||
		case GDK_KEY_o:
 | 
			
		||||
			on_open();
 | 
			
		||||
			return TRUE;
 | 
			
		||||
		case GDK_KEY_q:
 | 
			
		||||
		case GDK_KEY_w:
 | 
			
		||||
			gtk_widget_destroy(g.window);
 | 
			
		||||
			return TRUE;
 | 
			
		||||
 | 
			
		||||
		case GDK_KEY_question:
 | 
			
		||||
			show_help_shortcuts();
 | 
			
		||||
			return TRUE;
 | 
			
		||||
		case GDK_KEY_comma:
 | 
			
		||||
			show_preferences();
 | 
			
		||||
			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);
 | 
			
		||||
	g_free(accelerator);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): See how Unity 7 behaves,
 | 
			
		||||
	// we might want to keep GtkApplicationWindow:show-menubar then.
 | 
			
		||||
	gboolean shell_shows_menubar = FALSE;
 | 
			
		||||
	(void) g_object_get(gtk_settings_get_default(),
 | 
			
		||||
		"gtk-shell-shows-menubar", &shell_shows_menubar, NULL);
 | 
			
		||||
 | 
			
		||||
	guint mask = gtk_accelerator_get_default_mod_mask();
 | 
			
		||||
	if (key && event->keyval == key && (event->state & mask) == mods) {
 | 
			
		||||
	if (key && event->keyval == key && (event->state & mask) == mods &&
 | 
			
		||||
		!shell_shows_menubar) {
 | 
			
		||||
		gtk_widget_show(g.menu);
 | 
			
		||||
 | 
			
		||||
		// _gtk_menu_shell_set_keyboard_mode() is private.
 | 
			
		||||
@@ -1476,6 +1500,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
 | 
			
		||||
	return FALSE;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Cursor keys, e.g., simply cannot be bound through accelerators
 | 
			
		||||
// (and GtkWidget::keynav-failed would arguably be an awful solution).
 | 
			
		||||
//
 | 
			
		||||
// GtkBindingSets can be added directly through GtkStyleContext,
 | 
			
		||||
// but that would still require setting up action signals on the widget class,
 | 
			
		||||
// which is extremely cumbersome.  GtkWidget::move-focus has no return value,
 | 
			
		||||
// so we can't override that and abort further handling.
 | 
			
		||||
//
 | 
			
		||||
// Therefore, bind directly to keypresses.  Order can be fine-tuned with
 | 
			
		||||
// g_signal_connect{,after}(), or overriding the handler and either tactically
 | 
			
		||||
// chaining up or using gtk_window_propagate_key_event().
 | 
			
		||||
static gboolean
 | 
			
		||||
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
 | 
			
		||||
	G_GNUC_UNUSED gpointer data)
 | 
			
		||||
@@ -1654,6 +1689,9 @@ make_toolbar_radio(const char *label, const char *tooltip)
 | 
			
		||||
	GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
 | 
			
		||||
	gtk_widget_set_tooltip_text(button, tooltip);
 | 
			
		||||
	gtk_widget_set_focus_on_click(button, FALSE);
 | 
			
		||||
	gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(button), FALSE);
 | 
			
		||||
	gtk_style_context_add_class(
 | 
			
		||||
		gtk_widget_get_style_context(button), GTK_STYLE_CLASS_FLAT);
 | 
			
		||||
	return button;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1715,7 +1753,6 @@ make_browser_toolbar(void)
 | 
			
		||||
		gtk_radio_button_join_group(radio, last);
 | 
			
		||||
		last = radio;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return browser_toolbar;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1982,41 +2019,178 @@ make_browser_sidebar(FivIoModel *model)
 | 
			
		||||
	return sidebar;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static GtkWidget *
 | 
			
		||||
make_menu_bar(void)
 | 
			
		||||
// --- Actions -----------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
#define ACTION(name) static void on_action_ ## name(void)
 | 
			
		||||
 | 
			
		||||
ACTION(new_window) {
 | 
			
		||||
	if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
 | 
			
		||||
		spawn_uri(g.uri);
 | 
			
		||||
	else
 | 
			
		||||
		spawn_uri(g.directory);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ACTION(quit) {
 | 
			
		||||
	gtk_widget_destroy(g.window);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ACTION(location) {
 | 
			
		||||
	fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ACTION(preferences) {
 | 
			
		||||
	show_preferences(g.window);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ACTION(about) {
 | 
			
		||||
	show_about_dialog(g.window);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
	const char *name;                   ///< Unprefixed action name
 | 
			
		||||
	GCallback callback;                 ///< Simple callback
 | 
			
		||||
	const char **accels;                ///< NULL-terminated accelerator list
 | 
			
		||||
} ActionEntry;
 | 
			
		||||
 | 
			
		||||
static ActionEntry g_actions[] = {
 | 
			
		||||
	{"preferences", on_action_preferences,
 | 
			
		||||
		(const char *[]) {"<Primary>comma", NULL}},
 | 
			
		||||
	{"new-window", on_action_new_window,
 | 
			
		||||
		(const char *[]) {"<Primary>n", NULL}},
 | 
			
		||||
	{"open", on_open,
 | 
			
		||||
		(const char *[]) {"<Primary>o", "o", NULL}},
 | 
			
		||||
	{"quit", on_action_quit,
 | 
			
		||||
		(const char *[]) {"<Primary>q", "<Primary>w", "q", NULL}},
 | 
			
		||||
	{"toggle-fullscreen", toggle_fullscreen,
 | 
			
		||||
		(const char *[]) {"F11", "f", NULL}},
 | 
			
		||||
	{"toggle-sunlight", toggle_sunlight,
 | 
			
		||||
		(const char *[]) {"<Alt><Shift>d", NULL}},
 | 
			
		||||
	{"go-back", go_back,
 | 
			
		||||
		(const char *[]) {"<Alt>Left", "BackSpace", NULL}},
 | 
			
		||||
	{"go-forward", go_forward,
 | 
			
		||||
		(const char *[]) {"<Alt>Right", NULL}},
 | 
			
		||||
	{"go-location", on_action_location,
 | 
			
		||||
		(const char *[]) {"<Primary>l", NULL}},
 | 
			
		||||
	{"help", show_help_contents,
 | 
			
		||||
		(const char *[]) {"F1", NULL}},
 | 
			
		||||
	{"shortcuts", show_help_shortcuts,
 | 
			
		||||
		// Similar to win.show-help-overlay in gtkapplication.c.
 | 
			
		||||
		(const char *[]) {"<Primary>question", "<Primary>F1", NULL}},
 | 
			
		||||
	{"about", on_action_about,
 | 
			
		||||
		(const char *[]) {"<Shift>F1", NULL}},
 | 
			
		||||
	{}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
dispatch_action(G_GNUC_UNUSED GSimpleAction *action,
 | 
			
		||||
	G_GNUC_UNUSED GVariant *parameter, gpointer user_data)
 | 
			
		||||
{
 | 
			
		||||
	g.menu = gtk_menu_bar_new();
 | 
			
		||||
	GCallback callback = user_data;
 | 
			
		||||
	callback();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	GtkWidget *item_quit = gtk_menu_item_new_with_mnemonic("_Quit");
 | 
			
		||||
	g_signal_connect_swapped(item_quit, "activate",
 | 
			
		||||
		G_CALLBACK(gtk_widget_destroy), g.window);
 | 
			
		||||
static void
 | 
			
		||||
set_up_action(GtkApplication *app, const ActionEntry *a)
 | 
			
		||||
{
 | 
			
		||||
	GSimpleAction *action = g_simple_action_new(a->name, NULL);
 | 
			
		||||
	g_signal_connect(action, "activate",
 | 
			
		||||
		G_CALLBACK(dispatch_action), a->callback);
 | 
			
		||||
	g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action));
 | 
			
		||||
	g_object_unref(action);
 | 
			
		||||
 | 
			
		||||
	GtkWidget *menu_file = gtk_menu_new();
 | 
			
		||||
	gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit);
 | 
			
		||||
	GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File");
 | 
			
		||||
	gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_file), menu_file);
 | 
			
		||||
	gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_file);
 | 
			
		||||
	gchar *full_name = g_strdup_printf("app.%s", a->name);
 | 
			
		||||
	gtk_application_set_accels_for_action(app, full_name, a->accels);
 | 
			
		||||
	g_free(full_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	GtkWidget *item_contents = gtk_menu_item_new_with_mnemonic("_Contents");
 | 
			
		||||
	g_signal_connect_swapped(item_contents, "activate",
 | 
			
		||||
		G_CALLBACK(show_help_contents), NULL);
 | 
			
		||||
	GtkWidget *item_shortcuts =
 | 
			
		||||
		gtk_menu_item_new_with_mnemonic("_Keyboard Shortcuts");
 | 
			
		||||
	g_signal_connect_swapped(item_shortcuts, "activate",
 | 
			
		||||
		G_CALLBACK(show_help_shortcuts), NULL);
 | 
			
		||||
	GtkWidget *item_about = gtk_menu_item_new_with_mnemonic("_About");
 | 
			
		||||
	g_signal_connect_swapped(item_about, "activate",
 | 
			
		||||
		G_CALLBACK(show_about_dialog), g.window);
 | 
			
		||||
// --- Menu --------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
	GtkWidget *menu_help = gtk_menu_new();
 | 
			
		||||
	gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents);
 | 
			
		||||
	gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts);
 | 
			
		||||
	gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about);
 | 
			
		||||
	GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help");
 | 
			
		||||
	gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_help), menu_help);
 | 
			
		||||
	gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_help);
 | 
			
		||||
typedef struct {
 | 
			
		||||
	const char *label;                  ///< Label, with a mnemonic
 | 
			
		||||
	const char *action;                 ///< Prefixed action name
 | 
			
		||||
	gboolean macos;                     ///< Show in the macOS global menu?
 | 
			
		||||
} MenuItem;
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
	const char *label;                  ///< Label, with a mnemonic
 | 
			
		||||
	const MenuItem *items;              ///< ""-sectioned menu items
 | 
			
		||||
} MenuRoot;
 | 
			
		||||
 | 
			
		||||
// We're single-instance, skip the "win" namespace for simplicity.
 | 
			
		||||
static MenuRoot g_menu[] = {
 | 
			
		||||
	{"_File", (MenuItem[]) {
 | 
			
		||||
		{"_New Window", "app.new-window", TRUE},
 | 
			
		||||
		{"_Open...", "app.open", TRUE},
 | 
			
		||||
		{"", NULL, TRUE},
 | 
			
		||||
		{"_Quit", "app.quit", FALSE},
 | 
			
		||||
		{}
 | 
			
		||||
	}},
 | 
			
		||||
	{"_Go", (MenuItem[]) {
 | 
			
		||||
		{"_Back", "app.go-back", TRUE},
 | 
			
		||||
		{"_Forward", "app.go-forward", TRUE},
 | 
			
		||||
		{"", NULL, TRUE},
 | 
			
		||||
		{"_Location...", "app.go-location", TRUE},
 | 
			
		||||
		{}
 | 
			
		||||
	}},
 | 
			
		||||
	{"_Help", (MenuItem[]) {
 | 
			
		||||
		{"_Contents", "app.help", TRUE},
 | 
			
		||||
		{"_Keyboard Shortcuts", "app.shortcuts", TRUE},
 | 
			
		||||
		{"_About", "app.about", FALSE},
 | 
			
		||||
		{}
 | 
			
		||||
	}},
 | 
			
		||||
	{}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static GMenuModel *
 | 
			
		||||
make_submenu(const MenuItem *items)
 | 
			
		||||
{
 | 
			
		||||
	GMenu *menu = g_menu_new();
 | 
			
		||||
	while (items->label) {
 | 
			
		||||
		GMenu *section = g_menu_new();
 | 
			
		||||
		for (; items->label; items++) {
 | 
			
		||||
			// Empty strings are interpreted as separators.
 | 
			
		||||
			if (!*items->label) {
 | 
			
		||||
				items++;
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			GMenuItem *subitem = g_menu_item_new(items->label, items->action);
 | 
			
		||||
			if (!items->macos) {
 | 
			
		||||
				g_menu_item_set_attribute(
 | 
			
		||||
					subitem, "hidden-when", "s", "macos-menubar");
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			g_menu_append_item(section, subitem);
 | 
			
		||||
			g_object_unref(subitem);
 | 
			
		||||
		}
 | 
			
		||||
		g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
 | 
			
		||||
		g_object_unref(section);
 | 
			
		||||
	}
 | 
			
		||||
	return G_MENU_MODEL(menu);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static GMenuModel *
 | 
			
		||||
make_menu_model(void)
 | 
			
		||||
{
 | 
			
		||||
	GMenu *menu = g_menu_new();
 | 
			
		||||
	for (const MenuRoot *root = g_menu; root->label; root++) {
 | 
			
		||||
		GMenuModel *submenu = make_submenu(root->items);
 | 
			
		||||
		g_menu_append_submenu(menu, root->label, submenu);
 | 
			
		||||
		g_object_unref(submenu);
 | 
			
		||||
	}
 | 
			
		||||
	return G_MENU_MODEL(menu);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static GtkWidget *
 | 
			
		||||
make_menu_bar(GMenuModel *model)
 | 
			
		||||
{
 | 
			
		||||
	g.menu = gtk_menu_bar_new_from_model(model);
 | 
			
		||||
 | 
			
		||||
	// Don't let it take up space by default. Firefox sets a precedent here.
 | 
			
		||||
	// (gtk_application_window_set_show_menubar() doesn't seem viable for use
 | 
			
		||||
	// for this purpose.)
 | 
			
		||||
	gtk_widget_show_all(g.menu);
 | 
			
		||||
	gtk_widget_set_no_show_all(g.menu, TRUE);
 | 
			
		||||
	gtk_widget_hide(g.menu);
 | 
			
		||||
@@ -2024,6 +2198,8 @@ make_menu_bar(void)
 | 
			
		||||
	return g.menu;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Application -------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
// This is incredibly broken https://stackoverflow.com/a/51054396/76313
 | 
			
		||||
// thus resolving the problem using overlaps.
 | 
			
		||||
// We're trying to be universal for light and dark themes both. It's hard.
 | 
			
		||||
@@ -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); \
 | 
			
		||||
	fiv-view, fiv-browser { background: @content_view_bg; } \
 | 
			
		||||
	placessidebar.fiv box > separator { margin: 4px 0; } \
 | 
			
		||||
	placessidebar.fiv row { min-height: 2em; } \
 | 
			
		||||
	.fiv-toolbar button { padding-left: 0; padding-right: 0; } \
 | 
			
		||||
	.fiv-toolbar button.text-button { \
 | 
			
		||||
		padding-left: 4px; padding-right: 4px; } \
 | 
			
		||||
	.fiv-toolbar > button:first-child { padding-left: 4px; } \
 | 
			
		||||
	.fiv-toolbar > button:last-child { padding-right: 4px; } \
 | 
			
		||||
	.fiv-toolbar separator { \
 | 
			
		||||
@@ -2208,10 +2387,27 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
 | 
			
		||||
	g_signal_connect(g.window, "window-state-event",
 | 
			
		||||
		G_CALLBACK(on_window_state_event), NULL);
 | 
			
		||||
 | 
			
		||||
	for (const ActionEntry *a = g_actions; a->name; a++)
 | 
			
		||||
		set_up_action(GTK_APPLICATION(app), a);
 | 
			
		||||
 | 
			
		||||
	// GtkApplicationWindow overrides GtkContainer/GtkWidget virtual methods
 | 
			
		||||
	// so that it has the menu bar as an extra child (if it so decides).
 | 
			
		||||
	// However, we currently want this menu bar to only show on a key press,
 | 
			
		||||
	// and to hide as soon as it's no longer being used.
 | 
			
		||||
	// Messing with the window's internal state seems at best quirky,
 | 
			
		||||
	// so we'll manage the menu entirely by ourselves.
 | 
			
		||||
	gtk_application_window_set_show_menubar(
 | 
			
		||||
		GTK_APPLICATION_WINDOW(g.window), FALSE);
 | 
			
		||||
 | 
			
		||||
	GMenuModel *menu = make_menu_model();
 | 
			
		||||
	gtk_application_set_menubar(GTK_APPLICATION(app), menu);
 | 
			
		||||
	// The default "app menu" is good, in particular for macOS, so keep it.
 | 
			
		||||
 | 
			
		||||
	GtkWidget *menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
 | 
			
		||||
	gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar());
 | 
			
		||||
	gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar(menu));
 | 
			
		||||
	gtk_container_add(GTK_CONTAINER(menu_box), g.stack);
 | 
			
		||||
	gtk_container_add(GTK_CONTAINER(g.window), menu_box);
 | 
			
		||||
	g_object_unref(menu);
 | 
			
		||||
 | 
			
		||||
	GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
 | 
			
		||||
	if (g_settings_get_boolean(settings, "dark-theme"))
 | 
			
		||||
@@ -2256,7 +2452,7 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static struct {
 | 
			
		||||
	gboolean browse, extract_thumbnail;
 | 
			
		||||
	gboolean browse, collection, extract_thumbnail;
 | 
			
		||||
	gchar **args, *thumbnail_size, *thumbnail_size_search;
 | 
			
		||||
} o;
 | 
			
		||||
 | 
			
		||||
@@ -2266,12 +2462,12 @@ on_app_activate(
 | 
			
		||||
{
 | 
			
		||||
	// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
 | 
			
		||||
	// interpret multiple command line arguments differently, as a collection.
 | 
			
		||||
	// However, single-element collections are unrepresentable this way.
 | 
			
		||||
	// Should we allow multiple targets only in a special new mode?
 | 
			
		||||
	// However, single-element collections are unrepresentable this way,
 | 
			
		||||
	// so we have a switch to enforce it.
 | 
			
		||||
	g.files_index = -1;
 | 
			
		||||
	if (o.args) {
 | 
			
		||||
		const gchar *target = *o.args;
 | 
			
		||||
		if (o.args[1]) {
 | 
			
		||||
		if (o.args[1] || o.collection) {
 | 
			
		||||
			fiv_collection_reload(o.args);
 | 
			
		||||
			target = FIV_COLLECTION_SCHEME ":/";
 | 
			
		||||
		}
 | 
			
		||||
@@ -2387,11 +2583,6 @@ on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Use Little CMS with contexts instead.
 | 
			
		||||
#ifdef HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
	cmsPlugin(cmsFastFloatExtensions());
 | 
			
		||||
#endif  // HAVE_LCMS2_FAST_FLOAT
 | 
			
		||||
 | 
			
		||||
	// Normalize all arguments to URIs, and run thumbnailing modes first.
 | 
			
		||||
	for (gsize i = 0; o.args && o.args[i]; i++) {
 | 
			
		||||
		GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
 | 
			
		||||
@@ -2422,6 +2613,9 @@ main(int argc, char *argv[])
 | 
			
		||||
		{"browse", 0, G_OPTION_FLAG_IN_MAIN,
 | 
			
		||||
			G_OPTION_ARG_NONE, &o.browse,
 | 
			
		||||
			"Start in filesystem browsing mode", NULL},
 | 
			
		||||
		{"collection", 0, G_OPTION_FLAG_IN_MAIN,
 | 
			
		||||
			G_OPTION_ARG_NONE, &o.collection,
 | 
			
		||||
			"Always put arguments in a collection (implies --browse)", NULL},
 | 
			
		||||
		{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
 | 
			
		||||
			G_OPTION_ARG_NONE, NULL,
 | 
			
		||||
			"Invalidate the wide thumbnail cache", NULL},
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,13 @@
 | 
			
		||||
				double buffering.
 | 
			
		||||
			</description>
 | 
			
		||||
		</key>
 | 
			
		||||
		<key name='opengl' type='b'>
 | 
			
		||||
			<default>false</default>
 | 
			
		||||
			<summary>Use experimental OpenGL rendering</summary>
 | 
			
		||||
			<description>
 | 
			
		||||
				OpenGL within GTK+ is highly problematic--you don't want this.
 | 
			
		||||
			</description>
 | 
			
		||||
		</key>
 | 
			
		||||
		<key name='dark-theme' type='b'>
 | 
			
		||||
			<default>false</default>
 | 
			
		||||
			<summary>Use a dark theme variant on start-up</summary>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								fiv.wxs.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								fiv.wxs.in
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
<?xml version='1.0' encoding='utf-8'?>
 | 
			
		||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
 | 
			
		||||
	<?define FullName = "@ProjectName@ @ProjectVersion@" ?>
 | 
			
		||||
	<?if $(sys.BUILDARCH) = x64 ?>
 | 
			
		||||
		<?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
 | 
			
		||||
	<?else?>
 | 
			
		||||
		<?define ProgramFilesFolder = "ProgramFilesFolder" ?>
 | 
			
		||||
	<?endif?>
 | 
			
		||||
 | 
			
		||||
	<Product Id='*'
 | 
			
		||||
		Name='$(var.FullName)'
 | 
			
		||||
		UpgradeCode='a3e64e2d-4310-4c5f-8562-bb0e0b3e0a53'
 | 
			
		||||
		Language='1033'
 | 
			
		||||
		Codepage='1252'
 | 
			
		||||
		Version='@ProjectVersion@'
 | 
			
		||||
		Manufacturer='Premysl Eric Janouch'>
 | 
			
		||||
 | 
			
		||||
		<Package Id='*'
 | 
			
		||||
			Keywords='Installer,Image,Viewer'
 | 
			
		||||
			Description='$(var.FullName) Installer'
 | 
			
		||||
			Manufacturer='Premysl Eric Janouch'
 | 
			
		||||
			InstallerVersion='200'
 | 
			
		||||
			Compressed='yes'
 | 
			
		||||
			Languages='1033'
 | 
			
		||||
			SummaryCodepage='1252' />
 | 
			
		||||
 | 
			
		||||
		<Media Id='1' Cabinet='data.cab' EmbedCab='yes' />
 | 
			
		||||
		<Icon Id='fiv.ico' SourceFile='fiv.ico' />
 | 
			
		||||
		<Property Id='ARPPRODUCTICON' Value='fiv.ico' />
 | 
			
		||||
		<Property Id='ARPURLINFOABOUT' Value='@ProjectURL@' />
 | 
			
		||||
 | 
			
		||||
		<UIRef Id='WixUI_Minimal' />
 | 
			
		||||
		<!-- This isn't supported by msitools, but is necessary for WiX.
 | 
			
		||||
		<WixVariable Id='WixUILicenseRtf' Value='License.rtf' />
 | 
			
		||||
		-->
 | 
			
		||||
 | 
			
		||||
		<Directory Id='TARGETDIR' Name='SourceDir'>
 | 
			
		||||
			<Directory Id='$(var.ProgramFilesFolder)'>
 | 
			
		||||
				<Directory Id='INSTALLDIR' Name='$(var.FullName)' />
 | 
			
		||||
			</Directory>
 | 
			
		||||
 | 
			
		||||
			<Directory Id='ProgramMenuFolder'>
 | 
			
		||||
				<Directory Id='ProgramMenuDir' Name='$(var.FullName)' />
 | 
			
		||||
			</Directory>
 | 
			
		||||
 | 
			
		||||
			<Directory Id='DesktopFolder' />
 | 
			
		||||
		</Directory>
 | 
			
		||||
 | 
			
		||||
		<DirectoryRef Id='ProgramMenuDir'>
 | 
			
		||||
			<Component Id='ProgramMenuDir' Guid='*'>
 | 
			
		||||
				<Shortcut Id='ProgramsMenuShortcut'
 | 
			
		||||
					Name='@ProjectName@'
 | 
			
		||||
					Target='[INSTALLDIR]\fiv.exe'
 | 
			
		||||
					WorkingDirectory='INSTALLDIR'
 | 
			
		||||
					Arguments='"%USERPROFILE%"'
 | 
			
		||||
					Icon='fiv.ico' />
 | 
			
		||||
				<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
 | 
			
		||||
				<RegistryValue Root='HKCU'
 | 
			
		||||
					Key='Software\[Manufacturer]\[ProductName]'
 | 
			
		||||
					Type='string'
 | 
			
		||||
					Value=''
 | 
			
		||||
					KeyPath='yes' />
 | 
			
		||||
			</Component>
 | 
			
		||||
		</DirectoryRef>
 | 
			
		||||
 | 
			
		||||
		<Feature Id='Complete' Level='1'>
 | 
			
		||||
			<ComponentGroupRef Id='CG.fiv' />
 | 
			
		||||
			<ComponentRef Id='ProgramMenuDir' />
 | 
			
		||||
		</Feature>
 | 
			
		||||
	</Product>
 | 
			
		||||
</Wix>
 | 
			
		||||
							
								
								
									
										101
									
								
								meson.build
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								meson.build
									
									
									
									
									
								
							@@ -1,7 +1,8 @@
 | 
			
		||||
# vim: noet ts=4 sts=4 sw=4:
 | 
			
		||||
project('fiv', 'c',
 | 
			
		||||
	default_options : ['c_std=gnu99', 'warning_level=2'],
 | 
			
		||||
	version : '0.1.0')
 | 
			
		||||
	version : '1.0.0',
 | 
			
		||||
	meson_version : '>=0.57')
 | 
			
		||||
 | 
			
		||||
cc = meson.get_compiler('c')
 | 
			
		||||
add_project_arguments(
 | 
			
		||||
@@ -35,7 +36,9 @@ gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
 | 
			
		||||
dependencies = [
 | 
			
		||||
	dependency('gtk+-3.0'),
 | 
			
		||||
	dependency('pixman-1'),
 | 
			
		||||
	dependency('epoxy'),
 | 
			
		||||
 | 
			
		||||
	dependency('libjpeg'),
 | 
			
		||||
	dependency('libturbojpeg'),
 | 
			
		||||
	dependency('libwebp'),
 | 
			
		||||
	dependency('libwebpdemux'),
 | 
			
		||||
@@ -92,11 +95,13 @@ endif
 | 
			
		||||
# XXX: https://github.com/mesonbuild/meson/issues/825
 | 
			
		||||
docdir = get_option('datadir') / 'doc' / meson.project_name()
 | 
			
		||||
application_ns = 'name.janouch.'
 | 
			
		||||
application_url = 'https://janouch.name/p/' + meson.project_name()
 | 
			
		||||
 | 
			
		||||
conf = configuration_data()
 | 
			
		||||
conf.set_quoted('PROJECT_NAME', meson.project_name())
 | 
			
		||||
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
 | 
			
		||||
conf.set_quoted('PROJECT_NS', application_ns)
 | 
			
		||||
conf.set_quoted('PROJECT_URL', application_url)
 | 
			
		||||
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
 | 
			
		||||
if win32
 | 
			
		||||
	conf.set_quoted('PROJECT_DOCDIR', docdir)
 | 
			
		||||
@@ -138,7 +143,8 @@ if win32
 | 
			
		||||
				'--width', size, '--height', size, '@INPUT@'])
 | 
			
		||||
	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@'])
 | 
			
		||||
	rc += windows.compile_resources('fiv.rc', depends : icon_ico)
 | 
			
		||||
endif
 | 
			
		||||
@@ -153,16 +159,21 @@ gresources = gnome.compile_resources('resources',
 | 
			
		||||
tiff_tables = custom_target('tiff-tables.h',
 | 
			
		||||
	output : 'tiff-tables.h',
 | 
			
		||||
	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,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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-io-model.c', 'xdg.c', tiff_tables, gresources, rc, config,
 | 
			
		||||
	install : true,
 | 
			
		||||
	'fiv-io-model.c', gresources, rc, config,
 | 
			
		||||
	objects : iolib,
 | 
			
		||||
	dependencies : dependencies,
 | 
			
		||||
	install : true,
 | 
			
		||||
	win_subsystem : 'windows',
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -177,44 +188,55 @@ jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if get_option('tools').enabled()
 | 
			
		||||
	# libjq 1.6 lacks a pkg-config file, and there is no release in sight.
 | 
			
		||||
	# libjq 1.6 is required.
 | 
			
		||||
	# libjq has only received a pkg-config file in version 1.7.
 | 
			
		||||
	# libjq >= 1.6 is required.
 | 
			
		||||
	tools_dependencies = [
 | 
			
		||||
		cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
 | 
			
		||||
	tools_c_args = cc.get_supported_arguments(
 | 
			
		||||
		'-Wno-unused-function', '-Wno-unused-parameter')
 | 
			
		||||
	foreach tool : ['info', 'pnginfo', 'rawinfo']
 | 
			
		||||
	foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
 | 
			
		||||
		executable(tool, 'tools/' + tool + '.c', tiff_tables,
 | 
			
		||||
			dependencies : tools_dependencies,
 | 
			
		||||
			c_args: tools_c_args)
 | 
			
		||||
	endforeach
 | 
			
		||||
 | 
			
		||||
	if gdkpixbuf.found()
 | 
			
		||||
		executable('benchmark-io', 'tools/benchmark-io.c', 'fiv-io.c', 'xdg.c',
 | 
			
		||||
			tiff_tables, dependencies : [dependencies, gdkpixbuf])
 | 
			
		||||
		executable('benchmark-io', 'tools/benchmark-io.c',
 | 
			
		||||
			objects : iolib,
 | 
			
		||||
			dependencies : [dependencies, gdkpixbuf])
 | 
			
		||||
	endif
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
# Copying the files to the build directory makes GSettings find them in devenv.
 | 
			
		||||
gsettings_schemas = ['fiv.gschema.xml']
 | 
			
		||||
foreach schema : gsettings_schemas
 | 
			
		||||
	install_data(schema,
 | 
			
		||||
		rename : [application_ns + schema],
 | 
			
		||||
	configure_file(
 | 
			
		||||
		input : schema,
 | 
			
		||||
		output : application_ns + schema,
 | 
			
		||||
		copy : true,
 | 
			
		||||
		install: true,
 | 
			
		||||
		install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
 | 
			
		||||
endforeach
 | 
			
		||||
 | 
			
		||||
# For the purposes of development: make the program find its GSettings schemas.
 | 
			
		||||
gnome.compile_schemas(depend_files : files(gsettings_schemas))
 | 
			
		||||
gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
 | 
			
		||||
 | 
			
		||||
# Validate various files, if there are tools around to do it.
 | 
			
		||||
xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
 | 
			
		||||
	gsettings_schemas
 | 
			
		||||
xmls += run_command(find_program('sed', required : false, disabler : true),
 | 
			
		||||
	'-n', 's@.*>\([^<>]*[.]svg\)<.*@resources/\\1@p',
 | 
			
		||||
# Meson is broken on Windows and removes the backslashes, so this ends up empty.
 | 
			
		||||
symbolics = run_command(find_program('sed', required : false, disabler : true),
 | 
			
		||||
	'-n', 's@.*>\\([^<>]*[.]svg\\)<.*@resources/\\1@p',
 | 
			
		||||
	configure_file(
 | 
			
		||||
		input : 'resources/resources.gresource.xml',
 | 
			
		||||
		output : 'resources.gresource.xml.stamp',
 | 
			
		||||
		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)
 | 
			
		||||
xmllint = find_program('xmllint', required : false, disabler : true)
 | 
			
		||||
@@ -317,11 +339,44 @@ if not win32
 | 
			
		||||
	if not meson.is_cross_build()
 | 
			
		||||
		meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
 | 
			
		||||
	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({
 | 
			
		||||
		'WINEPATH' : msys2_root / 'bin',
 | 
			
		||||
		'XDG_DATA_DIRS' : msys2_root / 'share',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,26 @@
 | 
			
		||||
#!/bin/sh -e
 | 
			
		||||
# msys2-cross-configure.sh: set up an MSYS2-based cross-compiled Meson build.
 | 
			
		||||
# Dependencies: AWK, sed, sha256sum, cURL, bsdtar,
 | 
			
		||||
# msys2-configure.sh: set up an MSYS2-based Meson build (x86-64 by default)
 | 
			
		||||
#
 | 
			
		||||
# Dependencies: AWK, sed, coreutils, cURL, bsdtar (libarchive),
 | 
			
		||||
# 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() {
 | 
			
		||||
	echo "$(tput bold)-- $*$(tput sgr0)"
 | 
			
		||||
@@ -10,7 +28,7 @@ status() {
 | 
			
		||||
 | 
			
		||||
dbsync() {
 | 
			
		||||
	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%"] }
 | 
			
		||||
		NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
 | 
			
		||||
		!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
 | 
			
		||||
@@ -28,10 +46,11 @@ fetch() {
 | 
			
		||||
	} BEGIN { while ((getline < "db.tsv") > 0) {
 | 
			
		||||
		filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
 | 
			
		||||
			gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
 | 
			
		||||
	} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | while IFS= read -r name
 | 
			
		||||
	} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
 | 
			
		||||
	while IFS= read -r name
 | 
			
		||||
	do
 | 
			
		||||
		status Fetching "$name"
 | 
			
		||||
		[ -f "packages/$name" ] || curl -#o "packages/$name" "$repository/$name"
 | 
			
		||||
		[ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
 | 
			
		||||
	done
 | 
			
		||||
 | 
			
		||||
	version=$(curl -# https://exiftool.org/ver.txt)
 | 
			
		||||
@@ -51,14 +70,20 @@ extract() {
 | 
			
		||||
	for subdir in *
 | 
			
		||||
	do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
 | 
			
		||||
	done
 | 
			
		||||
	for i in packages/*
 | 
			
		||||
	do bsdtar -xf "$i" --strip-components 1 mingw64
 | 
			
		||||
	done
 | 
			
		||||
	while IFS= read -r name
 | 
			
		||||
	do bsdtar -xf "packages/$name" --strip-components 1 \
 | 
			
		||||
		--exclude '*/share/man' --exclude '*/share/doc'
 | 
			
		||||
	done < db.want
 | 
			
		||||
 | 
			
		||||
	bsdtar -xf exiftool.tar.gz
 | 
			
		||||
	mv Image-ExifTool-*/exiftool bin
 | 
			
		||||
	mv Image-ExifTool-*/lib/* lib/perl5/site_perl
 | 
			
		||||
	rm -rf Image-ExifTool-*
 | 
			
		||||
	# Don't require Perl, which may not exist anymore on i686:
 | 
			
		||||
	# https://github.com/msys2/MINGW-packages/pull/20085
 | 
			
		||||
	if [ -d lib/perl5 ]
 | 
			
		||||
	then
 | 
			
		||||
		bsdtar -xf exiftool.tar.gz
 | 
			
		||||
		mv Image-ExifTool-*/exiftool bin
 | 
			
		||||
		mv Image-ExifTool-*/lib/* lib/perl5/site_perl
 | 
			
		||||
		rm -rf Image-ExifTool-*
 | 
			
		||||
	fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
configure() {
 | 
			
		||||
@@ -74,49 +99,50 @@ configure() {
 | 
			
		||||
 | 
			
		||||
setup() {
 | 
			
		||||
	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
 | 
			
		||||
	[binaries]
 | 
			
		||||
	c = 'x86_64-w64-mingw32-gcc'
 | 
			
		||||
	cpp = 'x86_64-w64-mingw32-g++'
 | 
			
		||||
	ar = 'x86_64-w64-mingw32-gcc-ar'
 | 
			
		||||
	ranlib = 'x86_64-w64-mingw32-gcc-ranlib'
 | 
			
		||||
	strip = 'x86_64-w64-mingw32-strip'
 | 
			
		||||
	windres = 'x86_64-w64-mingw32-windres'
 | 
			
		||||
	c = '$chost-gcc'
 | 
			
		||||
	cpp = '$chost-g++'
 | 
			
		||||
	ar = '$chost-gcc-ar'
 | 
			
		||||
	ranlib = '$chost-gcc-ranlib'
 | 
			
		||||
	strip = '$chost-strip'
 | 
			
		||||
	windres = '$chost-windres'
 | 
			
		||||
	pkgconfig = 'pkg-config'
 | 
			
		||||
 | 
			
		||||
	[properties]
 | 
			
		||||
	sys_root = '$builddir'
 | 
			
		||||
	msys2_root = '$msys2_root'
 | 
			
		||||
	pkg_config_libdir = '$msys2_root/share/pkgconfig:$msys2_root/lib/pkgconfig'
 | 
			
		||||
	needs_exe_wrapper = true
 | 
			
		||||
	pkg_config_libdir = '$pclibdir'
 | 
			
		||||
	needs_exe_wrapper = $wrap
 | 
			
		||||
 | 
			
		||||
	[host_machine]
 | 
			
		||||
	system = 'windows'
 | 
			
		||||
	cpu_family = 'x86_64'
 | 
			
		||||
	cpu = 'x86_64'
 | 
			
		||||
	cpu_family = '$carch'
 | 
			
		||||
	cpu = '$carch'
 | 
			
		||||
	endian = 'little'
 | 
			
		||||
	EOF
 | 
			
		||||
 | 
			
		||||
	meson setup --buildtype=debugoptimized --prefix="$packagedir" \
 | 
			
		||||
	meson setup --buildtype=debugoptimized --prefix=/ \
 | 
			
		||||
		--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sourcedir=$(realpath "${2:-$(dirname "$0")}")
 | 
			
		||||
builddir=$(realpath "${1:-builddir}")
 | 
			
		||||
packagedir=$builddir/package
 | 
			
		||||
toolchain=$builddir/msys2-cross-toolchain.meson
 | 
			
		||||
 | 
			
		||||
# This directory name matches the prefix in .pc files, so we don't need to
 | 
			
		||||
# 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"
 | 
			
		||||
cd "$msys2_root"
 | 
			
		||||
dbsync
 | 
			
		||||
fetch mingw-w64-x86_64-gtk3 mingw-w64-x86_64-lcms2 \
 | 
			
		||||
	mingw-w64-x86_64-libraw mingw-w64-x86_64-libheif \
 | 
			
		||||
	mingw-w64-x86_64-perl mingw-w64-x86_64-perl-win32-api \
 | 
			
		||||
	mingw-w64-x86_64-libwinpthread-git # Because we don't do "provides"?
 | 
			
		||||
fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-libjxl $pkg-perl \
 | 
			
		||||
	$pkg-perl-win32-api $pkg-libwinpthread-git # Because we don't do "provides"?
 | 
			
		||||
verify
 | 
			
		||||
extract
 | 
			
		||||
configure
 | 
			
		||||
@@ -3,25 +3,33 @@ export LC_ALL=C
 | 
			
		||||
cd "$MESON_INSTALL_DESTDIR_PREFIX"
 | 
			
		||||
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.
 | 
			
		||||
cp -p "$msys2_root"/bin/*.dll .
 | 
			
		||||
cp -p "$msys2_root"/bin/wperl.exe .
 | 
			
		||||
cp -p "$msys2_root"/bin/exiftool .
 | 
			
		||||
cp -p "$msys2_root"/bin/wperl.exe . || :
 | 
			
		||||
cp -p "$msys2_root"/bin/exiftool . || :
 | 
			
		||||
# The console helper is only useful for debug builds.
 | 
			
		||||
cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
 | 
			
		||||
cp -pR "$msys2_root"/etc/ .
 | 
			
		||||
 | 
			
		||||
mkdir -p lib
 | 
			
		||||
cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib
 | 
			
		||||
cp -pR "$msys2_root"/lib/perl5/ lib
 | 
			
		||||
cp -pR "$msys2_root"/lib/perl5/ lib || :
 | 
			
		||||
mkdir -p share/glib-2.0/schemas
 | 
			
		||||
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
 | 
			
		||||
mkdir -p share
 | 
			
		||||
cp -pR "$msys2_root"/share/mime/ share
 | 
			
		||||
mkdir -p share/icons
 | 
			
		||||
cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons
 | 
			
		||||
mkdir -p share/icons/hicolor
 | 
			
		||||
cp -p "$msys2_root"/share/icons/hicolor/index.theme share/icons/hicolor
 | 
			
		||||
mkdir -p share/mime
 | 
			
		||||
# GIO doesn't use the database on Windows, this subset is for us.
 | 
			
		||||
find "$msys2_root"/share/mime/ -maxdepth 1 -type f -exec cp -p {} share/mime \;
 | 
			
		||||
 | 
			
		||||
# Remove unreferenced libraries.
 | 
			
		||||
find lib -name '*.a' -exec rm -- {} +
 | 
			
		||||
							
								
								
									
										35
									
								
								msys2-package.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										35
									
								
								msys2-package.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
#!/bin/sh -e
 | 
			
		||||
export LC_ALL=C
 | 
			
		||||
cd "$MESON_BUILD_ROOT"
 | 
			
		||||
arch=$1 msi=$2 files=package-files.wxs
 | 
			
		||||
destdir=$(pwd)/package/${msi%.*}
 | 
			
		||||
shift 2
 | 
			
		||||
 | 
			
		||||
# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
 | 
			
		||||
[ "$arch" = "x86" ] || arch=x64
 | 
			
		||||
 | 
			
		||||
rm -rf "$destdir"
 | 
			
		||||
meson install --destdir "$destdir"
 | 
			
		||||
 | 
			
		||||
txt2rtf() {
 | 
			
		||||
	LC_ALL=C.UTF-8 iconv -f utf-8 -t ascii//translit "$@" | awk 'BEGIN {
 | 
			
		||||
		print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
 | 
			
		||||
		print "\\f0\\fs24{\\pard\\sa240"
 | 
			
		||||
	} {
 | 
			
		||||
		gsub(/\\/, "\\\\"); gsub(/[{]/, "\\{"); gsub(/[}]/, "\\}")
 | 
			
		||||
		if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
 | 
			
		||||
		else { print prefix $0; prefix = " " }
 | 
			
		||||
	} END {
 | 
			
		||||
		print "\\par}}"
 | 
			
		||||
	}'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# msitools have this filename hardcoded in UI files, and it's required.
 | 
			
		||||
txt2rtf "$MESON_SOURCE_ROOT/LICENSE" > License.rtf
 | 
			
		||||
 | 
			
		||||
find "$destdir" -type f \
 | 
			
		||||
	| wixl-heat --prefix "$destdir/" --directory-ref INSTALLDIR \
 | 
			
		||||
		--component-group CG.fiv --var var.SourceDir > "$files"
 | 
			
		||||
 | 
			
		||||
wixl --verbose --arch "$arch" -D SourceDir="$destdir" --ext ui \
 | 
			
		||||
	--output "$msi" "$@" "$files"
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
[wrap-file]
 | 
			
		||||
directory = jpeg-quantsmooth-1.20210408
 | 
			
		||||
source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20210408.tar.gz
 | 
			
		||||
source_filename = jpeg-quantsmooth-1.20210408.tar.gz
 | 
			
		||||
source_hash = 5937ca26db33888cab8638c1a8dc7a367a953bd0857ceb1290d5abc6febf3116
 | 
			
		||||
directory = jpeg-quantsmooth-1.20230818
 | 
			
		||||
source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20230818.tar.gz
 | 
			
		||||
source_filename = jpeg-quantsmooth-1.20230818.tar.gz
 | 
			
		||||
source_hash = ff9a62e8560851648c60d84b3d97ebd9769f01ce6b995779e071d19a759eca06
 | 
			
		||||
patch_directory = libjpegqs
 | 
			
		||||
 | 
			
		||||
[provide]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
# vim: noet ts=4 sts=4 sw=4:
 | 
			
		||||
project('jpeg-qs', 'c')
 | 
			
		||||
add_project_arguments(meson.get_compiler('c')
 | 
			
		||||
	.get_supported_arguments('-Wno-misleading-indentation'),
 | 
			
		||||
	'-DWITH_LOG', language : 'c')
 | 
			
		||||
add_project_arguments('-DWITH_LOG', language : 'c')
 | 
			
		||||
 | 
			
		||||
deps = [
 | 
			
		||||
	dependency('libjpeg'),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								tiffer.h
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								tiffer.h
									
									
									
									
									
								
							@@ -156,12 +156,18 @@ tiffer_next_ifd(struct tiffer *self)
 | 
			
		||||
	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.
 | 
			
		||||
static bool
 | 
			
		||||
tiffer_subifd(
 | 
			
		||||
	const struct tiffer *self, uint32_t offset, struct tiffer *subreader)
 | 
			
		||||
{
 | 
			
		||||
	if (self->end - self->begin < offset)
 | 
			
		||||
	if (tiffer_length(self) < offset)
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	*subreader = *self;
 | 
			
		||||
@@ -332,7 +338,7 @@ tiffer_next_entry(struct tiffer *self, struct tiffer_entry *entry)
 | 
			
		||||
	if (values_size <= sizeof offset) {
 | 
			
		||||
		entry->p = self->p;
 | 
			
		||||
		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;
 | 
			
		||||
	} else {
 | 
			
		||||
		return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -41,14 +41,14 @@ one_file(const char *filename)
 | 
			
		||||
		.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_free((char *) ctx.uri);
 | 
			
		||||
	g_ptr_array_free(ctx.warnings, TRUE);
 | 
			
		||||
	if (!loaded_by_us)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	cairo_surface_destroy(loaded_by_us);
 | 
			
		||||
	fiv_io_image_unref(loaded_by_us);
 | 
			
		||||
	us = timestamp() - since_us;
 | 
			
		||||
 | 
			
		||||
	double since_pixbuf = timestamp(), pixbuf = 0;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										210
									
								
								tools/hotpixels.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								tools/hotpixels.c
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
			
		||||
//
 | 
			
		||||
// hotpixels.c: look for hot pixels in raw image files
 | 
			
		||||
//
 | 
			
		||||
// Usage: pass a bunch of raw photo images taken with the lens cap on at,
 | 
			
		||||
// e.g., ISO 8000-12800 @ 1/20-1/60, and store the resulting file as,
 | 
			
		||||
// e.g., Nikon D7500.badpixels, which can then be directly used by Rawtherapee.
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
//
 | 
			
		||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
//
 | 
			
		||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
			
		||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
			
		||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
			
		||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
			
		||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
#include <libraw.h>
 | 
			
		||||
 | 
			
		||||
#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
 | 
			
		||||
#error LibRaw 0.21.0 or newer is required.
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <errno.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
 | 
			
		||||
static void *
 | 
			
		||||
xreallocarray(void *o, size_t n, size_t m)
 | 
			
		||||
{
 | 
			
		||||
	if (m && n > SIZE_MAX / m) {
 | 
			
		||||
		fprintf(stderr, "xreallocarray: %s\n", strerror(ENOMEM));
 | 
			
		||||
		exit(EXIT_FAILURE);
 | 
			
		||||
	}
 | 
			
		||||
	void *p = realloc(o, n * m);
 | 
			
		||||
	if (!p && n && m) {
 | 
			
		||||
		fprintf(stderr, "xreallocarray: %s\n", strerror(errno));
 | 
			
		||||
		exit(EXIT_FAILURE);
 | 
			
		||||
	}
 | 
			
		||||
	return p;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
struct coord { ushort x, y; };
 | 
			
		||||
 | 
			
		||||
static bool
 | 
			
		||||
coord_equals(struct coord a, struct coord b)
 | 
			
		||||
{
 | 
			
		||||
	return a.x == b.x && a.y == b.y;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static int
 | 
			
		||||
coord_cmp(const void *a, const void *b)
 | 
			
		||||
{
 | 
			
		||||
	const struct coord *ca = (const struct coord *) a;
 | 
			
		||||
	const struct coord *cb = (const struct coord *) b;
 | 
			
		||||
	return ca->y != cb->y
 | 
			
		||||
		? (int) ca->y - (int) cb->y
 | 
			
		||||
		: (int) ca->x - (int) cb->x;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct candidates {
 | 
			
		||||
	struct coord *xy;
 | 
			
		||||
	size_t len;
 | 
			
		||||
	size_t alloc;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
candidates_add(struct candidates *c, ushort x, ushort y)
 | 
			
		||||
{
 | 
			
		||||
	if (c->len == c->alloc) {
 | 
			
		||||
		c->alloc += 64;
 | 
			
		||||
		c->xy = xreallocarray(c->xy, sizeof *c->xy, c->alloc);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c->xy[c->len++] = (struct coord) {x, y};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
// A stretch of zeroes that is assumed to mean start of outliers.
 | 
			
		||||
#define SPAN 10
 | 
			
		||||
 | 
			
		||||
static const char *
 | 
			
		||||
process_raw(struct candidates *c, const uint8_t *p, size_t len)
 | 
			
		||||
{
 | 
			
		||||
	libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
 | 
			
		||||
	if (!iprc)
 | 
			
		||||
		return "failed to obtain a LibRaw handle";
 | 
			
		||||
 | 
			
		||||
	int err = 0;
 | 
			
		||||
	if ((err = libraw_open_buffer(iprc, p, len)) ||
 | 
			
		||||
		(err = libraw_unpack(iprc))) {
 | 
			
		||||
		libraw_close(iprc);
 | 
			
		||||
		return libraw_strerror(err);
 | 
			
		||||
	}
 | 
			
		||||
	if (!iprc->rawdata.raw_image) {
 | 
			
		||||
		libraw_close(iprc);
 | 
			
		||||
		return "only Bayer raws are supported, not Foveon";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make a histogram.
 | 
			
		||||
	uint64_t bins[USHRT_MAX] = {};
 | 
			
		||||
	for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
 | 
			
		||||
		for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
 | 
			
		||||
			ushort y = iprc->sizes.top_margin + yy;
 | 
			
		||||
			ushort x = iprc->sizes.left_margin + xx;
 | 
			
		||||
			bins[iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x]]++;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Detecting outliers is not completely straight-forward,
 | 
			
		||||
	// it may help to see the histogram.
 | 
			
		||||
	if (getenv("HOTPIXELS_HISTOGRAM")) {
 | 
			
		||||
		for (ushort i = 0; i < USHRT_MAX; i++)
 | 
			
		||||
			fprintf(stderr, "%u ", (unsigned) bins[i]);
 | 
			
		||||
		fputc('\n', stderr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Go to the first non-zero pixel value.
 | 
			
		||||
	size_t last = 0;
 | 
			
		||||
	for (; last < USHRT_MAX; last++)
 | 
			
		||||
		if (bins[last])
 | 
			
		||||
			break;
 | 
			
		||||
 | 
			
		||||
	// Find the last pixel value we assume to not be hot.
 | 
			
		||||
	for (; last < USHRT_MAX - SPAN - 1; last++) {
 | 
			
		||||
		uint64_t nonzero = 0;
 | 
			
		||||
		for (int i = 1; i <= SPAN; i++)
 | 
			
		||||
			nonzero += bins[last + i];
 | 
			
		||||
		if (!nonzero)
 | 
			
		||||
			break;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Store coordinates for all pixels above that value.
 | 
			
		||||
	for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
 | 
			
		||||
		for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
 | 
			
		||||
			ushort y = iprc->sizes.top_margin + yy;
 | 
			
		||||
			ushort x = iprc->sizes.left_margin + xx;
 | 
			
		||||
			if (iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x] > last)
 | 
			
		||||
				candidates_add(c, xx, yy);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	libraw_close(iprc);
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
static const char *
 | 
			
		||||
do_file(struct candidates *c, const char *filename)
 | 
			
		||||
{
 | 
			
		||||
	FILE *fp = fopen(filename, "rb");
 | 
			
		||||
	if (!fp)
 | 
			
		||||
		return strerror(errno);
 | 
			
		||||
 | 
			
		||||
	uint8_t *data = NULL, buf[256 << 10];
 | 
			
		||||
	size_t n, len = 0;
 | 
			
		||||
	while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
 | 
			
		||||
		data = xreallocarray(data, len + n, 1);
 | 
			
		||||
		memcpy(data + len, buf, n);
 | 
			
		||||
		len += n;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const char *err = ferror(fp)
 | 
			
		||||
		? strerror(errno)
 | 
			
		||||
		: process_raw(c, data, len);
 | 
			
		||||
 | 
			
		||||
	fclose(fp);
 | 
			
		||||
	free(data);
 | 
			
		||||
	return err;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int
 | 
			
		||||
main(int argc, char *argv[])
 | 
			
		||||
{
 | 
			
		||||
	struct candidates c = {};
 | 
			
		||||
	for (int i = 1; i < argc; i++) {
 | 
			
		||||
		const char *filename = argv[i], *err = do_file(&c, filename);
 | 
			
		||||
		if (err) {
 | 
			
		||||
			fprintf(stderr, "%s: %s\n", filename, err);
 | 
			
		||||
			return EXIT_FAILURE;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	qsort(c.xy, c.len, sizeof *c.xy, coord_cmp);
 | 
			
		||||
 | 
			
		||||
	// If it is detected in all passed photos, it is probably indeed bad.
 | 
			
		||||
	int count = 1;
 | 
			
		||||
	for (size_t i = 1; i <= c.len; i++) {
 | 
			
		||||
		if (i != c.len && coord_equals(c.xy[i - 1], c.xy[i])) {
 | 
			
		||||
			count++;
 | 
			
		||||
			continue;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (count == argc - 1)
 | 
			
		||||
			printf("%u %u\n", c.xy[i - 1].x, c.xy[i - 1].y);
 | 
			
		||||
 | 
			
		||||
		count = 1;
 | 
			
		||||
	}
 | 
			
		||||
	return 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								tools/info.h
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								tools/info.h
									
									
									
									
									
								
							@@ -123,6 +123,39 @@ parse_exif_subifds(const struct tiffer *T, struct tiffer_entry *entry,
 | 
			
		||||
	return a;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Implemented partially, out of curiosity--it is not particularly useful,
 | 
			
		||||
// because there is a ton more parsing to do here.
 | 
			
		||||
static bool
 | 
			
		||||
parse_exif_makernote(jv *v, const struct tiffer_entry *entry)
 | 
			
		||||
{
 | 
			
		||||
	if (!getenv("INFO_MAKERNOTE") ||
 | 
			
		||||
		entry->tag != Exif_MakerNote || entry->type != TIFFER_UNDEFINED)
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	struct tiffer T = {};
 | 
			
		||||
	if (entry->remaining_count >= 16 &&
 | 
			
		||||
		!memcmp(entry->p, "Nikon\x00\x02", 7) &&
 | 
			
		||||
		tiffer_init(&T, entry->p + 10, entry->remaining_count - 10) &&
 | 
			
		||||
		tiffer_next_ifd(&T)) {
 | 
			
		||||
		*v = parse_exif_ifd(&T, NULL);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	if (entry->remaining_count >= 16 &&
 | 
			
		||||
		!memcmp(entry->p, "Apple iOS\x00\x00\x01MM", 14)) {
 | 
			
		||||
		T.un = &tiffer_unbe;
 | 
			
		||||
		T.begin = T.p = entry->p + 14;
 | 
			
		||||
		T.end = entry->p + entry->remaining_count - 14;
 | 
			
		||||
		T.remaining_fields = 0;
 | 
			
		||||
 | 
			
		||||
		struct tiffer subT = {};
 | 
			
		||||
		if (tiffer_subifd(&T, 0, &subT)) {
 | 
			
		||||
			*v = parse_exif_ifd(&subT, NULL);
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static jv
 | 
			
		||||
parse_exif_ascii(struct tiffer_entry *entry)
 | 
			
		||||
{
 | 
			
		||||
@@ -198,6 +231,8 @@ parse_exif_entry(jv o, const struct tiffer *T, struct tiffer_entry *entry,
 | 
			
		||||
		v = parse_exif_subifds(T, entry, subentries);
 | 
			
		||||
	} else if (entry->type == TIFFER_ASCII) {
 | 
			
		||||
		v = parse_exif_extract_sole_array_element(parse_exif_ascii(entry));
 | 
			
		||||
	} else if (info_begin == exif_entries && parse_exif_makernote(&v, entry)) {
 | 
			
		||||
		// Already processed.
 | 
			
		||||
	} else if (entry->type == TIFFER_UNDEFINED && !info->values) {
 | 
			
		||||
		// Several Exif entries of UNDEFINED type contain single-byte numbers.
 | 
			
		||||
		v = parse_exif_undefined(entry);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user