Compare commits

..

601 Commits

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

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

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

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

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

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

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

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

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

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

Meson can also find libraries outside the subroot,
in particular the fast float plugin.
2023-06-22 04:06:38 +02:00
a4772ce319 Improve native MSYS2 build compatibility 2023-06-21 18:38:30 +02:00
0318424540 Handle LibTIFF errors correctly 2023-06-13 13:49:30 +02:00
8d5885bfdf Prevent a possibility of GdkPixbuf crashes 2023-06-13 13:36:24 +02:00
41b5ddc744 Fix thumbnailing with the GdkPixbuf loader 2023-06-13 13:21:03 +02:00
b308b5da18 Fix thumbnail extraction 2023-06-13 12:44:23 +02:00
1577961aa2 Improve compatibility with older dependencies 2023-06-10 11:52:49 +02:00
1fb42e689f Declare minimum Meson version
Due to our meson.add_install_script() usage, which results in a warning,
followed by an error.
2023-06-10 11:52:49 +02:00
8953e6beea Update comments 2023-06-09 13:13:17 +02:00
2e8bbf0e43 Improve LibRaw thumbnail choice
Make use of LibRaw 0.21.0's extended thumbnail API.
2023-06-09 12:47:41 +02:00
07d4ea2dde Optimize thumbnail extraction
Don't go over the same data twice.
2023-06-08 18:59:21 +02:00
a5b5e32c3b Refactor fiv_thumbnail_extract() 2023-06-08 18:59:20 +02:00
1e8fe1411b benchmark-io: ignore GdkPixbuf errors
Measuring up against that library is no longer that interesting.
2023-06-08 18:59:20 +02:00
274c5f6f66 benchmark-io: fix URI passing
g_filename_to_uri() doesn't support relative paths.
2023-06-08 18:59:20 +02:00
de377d3eae Move the image load benchmark under tools 2023-06-08 18:59:20 +02:00
34388b93ea info: decode JPEGs from all CR2 IFDs 2023-06-08 18:59:19 +02:00
7dda3bd1ed Make it possible to switch off our TIFF/EP loader
Slightly repurpose the "enhance" toggle, which doesn't particularly
make sense to run on a thumbnail.
2023-06-08 12:17:43 +02:00
a3a5eb33cf Unify non/enhanced JPEG loading code
And in so doing, add missing warning redirection to JPEG Quant Smooth,
as well as downscaling.

We still heavily depend on libjpeg-turbo.
2023-06-08 09:47:55 +02:00
ee202ca28b Fix enhancement of CMYK JPEGs
The conversion to RGB was done twice.
2023-06-07 21:57:51 +02:00
04db6ed6a1 Slightly clean up colour management
SVGs are now semi-managed.
2023-06-06 18:08:47 +02:00
d6e79cf976 With newer Little CMS, colour manage ARGB surfaces 2023-06-06 13:01:38 +02:00
6cc4ca1f44 Use Little CMS's alpha premultiplication feature
And do a little cleanup.
2023-06-06 12:20:03 +02:00
1c25cb411f Fix the remaining case in file renames monitoring 2023-06-06 08:31:29 +02:00
399c4bdf69 info: decode WebP dimensions 2023-06-05 21:04:24 +02:00
a9b34ca3f2 Unite most info tools into just one binary
Turn this into more of an fq alternative, when used with jq.

Also don't say that TIFF files are Exif.
2023-06-05 18:11:37 +02:00
bd92ad73ec rawinfo: add output dimensions and PAR 2023-06-05 15:42:38 +02:00
b3bc481172 rawinfo: descend into JPEG thumbnails 2023-06-05 15:42:38 +02:00
a3745df84b Fix monitoring of file renames 2023-06-05 15:42:38 +02:00
cc59e537da Update meson invocation to avoid warnings 2023-06-04 17:15:37 +02:00
338ae69121 Add support for the Little CMS fast float plugin
On a sample of JPEGs, it improved loading speed from ~0.26s to ~0.15s.

Unfortunately, it isn't normally installed.
2023-06-04 16:16:52 +02:00
1c61fcc5bc Move git submodules to a subdirectory 2023-06-04 12:57:47 +02:00
dd1d6647dc Shuffle code around 2023-06-04 12:10:36 +02:00
abf4f1a792 Convert to strictly non-unique GtkApplication
It's not pretty, but it works.
2023-06-04 12:10:36 +02:00
6a7c86a41b Remove a macOS rendering bug workaround
Most important Cairo bugs seem to have been fixed recently.
2023-06-04 12:10:35 +02:00
6277a32fe6 Avoid invisible browser entries 2023-06-04 10:36:42 +02:00
8f0576d6bc Update runtime dependencies 2023-06-03 21:36:45 +02:00
f56c40cf00 Update sidebar entries on change automatically
This makes it more consistent.
2023-06-02 07:44:42 +02:00
28a1237d62 Slightly optimize file monitoring event handling 2023-06-01 20:58:16 +02:00
4c8df56193 Distinguish removed files more prettily
It's still somewhat bad, but at least no longer ridiculous.
2023-06-01 19:11:20 +02:00
200485246b Process some GFileMonitor events
So far, it's rather crude.
2023-05-31 18:39:14 +02:00
2caebb7d19 Fix crash when right-clicking removed files 2023-05-31 14:46:06 +02:00
9d9d538fe6 Add trashing to file context menus 2023-05-30 10:20:09 +02:00
3bdffd03db tools: decode TIFF XMP fields as UTF-8
This is more space-efficient than an array of ASCII codepoints.

Perhaps more fields would make good use of specialized decoders,
just this one made listings particularly annoying to deal with,
and it may additionaly contain important metadata.
2023-05-28 11:06:44 +02:00
a710692725 Clean up namespace pollution 2023-05-28 10:13:04 +02:00
859736e5be Move FivIoModel to its own compilation unit 2023-05-28 09:33:03 +02:00
d5b2e43364 Clean up 2023-05-28 08:50:46 +02:00
33251eaca7 Load MPF images as pages 2023-05-28 08:12:37 +02:00
63311644da Move MPF constants and table to tiff-tables.db 2023-05-28 08:12:36 +02:00
8668e85623 Make MPF parsing a bit safer 2023-05-28 08:12:27 +02:00
902eaf5a01 Make TIFF parsing a bit safer
At least on 64-bit systems, 32-bit may still have holes.
2023-05-28 08:12:22 +02:00
df7c7b9f6b Fix build without tools 2023-05-26 15:50:54 +02:00
bb4b895cb5 Extract some full-size raw previews without LibRaw
Not all image/x-nikon-nef will work like this,
so don't claim their MIME type.
2023-05-26 15:32:34 +02:00
0f1c61ae33 Extract all raw subimages as pages
And add missing colour management.
2023-05-26 13:30:22 +02:00
0359ddf99f Add a tool to extract LibRaw file information 2023-05-26 13:30:22 +02:00
a93fc3f88d Make TIFF and JPEG info extractors co-recursive 2023-05-24 06:18:14 +02:00
d70aedffa8 Slightly expand TIFF tables 2023-05-24 06:07:38 +02:00
dba728e0c4 Improve TIFF handling within tools
Nikon NEFs violate TIFF Tech Note 1, and it is easy to detect.

Also guard against more pointer overflows, and fix a temporary array
being used outside of its scope (found by a compiler).
2023-05-22 20:56:28 +02:00
544722f8e0 Try not to thumbnail FIFOs
Unless there is a writer, this may block forever.

And if there is one, we're somewhat likely to break something.
2023-05-21 23:31:41 +02:00
00110a639a Avoid use of NULL picture data pointers
The sanitizer would scream, and LibRaw would rather confusingly
return I/O errors.
2023-05-21 23:31:41 +02:00
5af36f4954 Fix raw image thumbnailing speed regression
LibRaw started returning the largest possible thumbnail,
which in the case of NEFs may be a nearly full-scale image.
2023-05-21 23:31:34 +02:00
ac72a72afc Mildly optimize raw image handling
Don't claim an alpha channel when we don't use it.
2023-05-21 22:56:29 +02:00
a6560509d9 Revise documentation and help output
Split out clearly internal options.
2023-04-17 07:20:07 +02:00
44c28f00d6 Make supported media type listing output unique 2023-04-17 07:19:37 +02:00
cce2b6ba51 Fix history behaviour
When starting in A/B, then manually going up to A,
and back down to A/B, going back in history to A was impossible,
because it would actually end up being a /forward/ entry.
2023-04-16 15:08:36 +02:00
43363ea4bf Cleanup 2023-04-16 11:50:15 +02:00
00fa76cb69 Avoid needless data duplication
And turn the initial load hack into somewhat clean-looking.
2023-04-15 05:20:35 +02:00
5e10f0ba54 Fix a logic error 2023-04-14 07:53:21 +02:00
eb44b6fb91 Fix a memory leak on image loading 2023-04-14 07:34:26 +02:00
a012011631 Deduplicate file information structures 2023-04-14 07:31:03 +02:00
05ac3a0651 Check filesize when retrieving thumbnails
In particular, this handles screenshots from Rigol oscilloscopes,
which reuse the same name series with the same file modification time.
2023-04-14 05:24:57 +02:00
4317c7e581 Remove a comment
Upon closer thought, I don't know how to implement the idea reasonably.
2023-04-14 05:24:57 +02:00
8da5f807cf Move and extend the browser toolbar
This makes the user interface more cohesive, and easier to use.

Both toolbars should ideally be made configurable.
2023-04-11 06:33:22 +02:00
1b50a834a5 Add optional browser filename labels 2023-04-11 06:04:27 +02:00
38c19edc8b Bump Wuffs 2023-04-07 16:23:31 +02:00
c646864805 Add directory tree traversal functionality
Thus far merely bound to the [ and ] keys in the browser.
2023-04-05 00:09:53 +02:00
c2196df141 Bump copyright years 2023-03-30 20:46:08 +02:00
44f2f5d4f5 Make the mirror command intuitive 2023-03-25 11:52:33 +01:00
25c91f5a77 Add a note about default applications 2023-03-15 05:52:32 +01:00
796b05c9a5 Integrate online reverse image search
This makes use of our image processing capabilities in order to
turn arbitrary image files into normalized thumbnails,
upload them to a temporary host, and pass the resulting URI
to a search provider.

In future, fiv should ideally run the upload itself,
so that its status and any errors are obvious to the user,
as well as to get rid of the script's dependency on jq.
2023-03-15 05:52:32 +01:00
9286858573 Bump Wuffs 2023-03-07 20:26:04 +01:00
95bc91e020 Find system libraries using proper names
Fixes Meson warnings.
2023-03-03 12:14:06 +01:00
1754bbcf45 Bump Wuffs to a regular release 2023-01-28 12:43:08 +01:00
62d82f38c8 Fix LibRaw 0.21.0 build 2022-12-23 17:17:33 +01:00
c063d93dc7 Fix LibRaw 0.21.0 build 2022-12-23 17:04:15 +01:00
0317b88c87 Don't require asciidoctor or a2x, import liberty 2022-10-09 01:24:03 +02:00
1431188e27 Update a comment 2022-10-04 00:54:14 +02:00
9cdc641b0a Center view rotations/flips 2022-10-03 23:00:16 +02:00
7b88e89489 Allow dragging with the middle mouse button 2022-09-03 16:17:21 +02:00
9c2d659d89 Update .gitignore 2022-08-24 05:24:00 +02:00
9fb90607ad Add a traditional manual page for fiv 2022-08-24 05:23:34 +02:00
dd09af34b7 Make binaries say what git commit they come from
The manual skipping of the initial "v" from tag names is unfortunate,
but still a bit better than further cluttering up the build system.
2022-08-13 12:42:21 +02:00
dcbc8a90b4 meson.build: add a bunch of validating tests 2022-08-12 15:49:57 +02:00
7cbd24dd2f Bundle a fuller installation of Perl/ExifTool
wperl is necessary to get rid of the console window,
which is merely one of several issues with the PAR Packer-based
ExifTool bundle used in the last commit.

The Perl installation could be heavily trimmed down,
but it seems to require a very manual process.
2022-08-11 16:11:07 +02:00
1a163bdb8b Resolve a few issues with MSYS2 cross-builds
- Fix launching of subprocesses (missing gspawn helpers).
 - Discard unused GSettings schemas.
 - Make the program find its user guide.
 - Bundle a somewhat suboptimal version of ExifTool.
2022-08-11 16:10:04 +02:00
cf19f82875 Automate Windows builds, add icons to executables
Scripts have been ported from sdtui, and adjusted for Meson.

The port is broken through and through on WINE,
but sort-of works natively.
2022-08-11 00:19:59 +02:00
9c9453172a fiv-jpegcrop: add middle mouse button dragging
Parasitic gesture code previously discarded from fiv gets to live on.
2022-08-10 12:23:31 +02:00
973c024abe fiv-jpegcrop: without args, show an Open dialog
But let's keep it hidden from application lists for now.
2022-08-10 11:42:16 +02:00
dba2b9c982 Fix the argument list of a callback 2022-08-10 10:22:44 +02:00
b6e1dc4893 Overload the F9 keyboard shortcut
It doesn't make a lot of sense to be able to toggle invisible widgets,
so just make F9 toggle "the toolbar that can currently be seen".

The more permanent setting can be adjusted in GSettings.
2022-08-09 18:03:55 +02:00
f94550ec61 Improve XDG path handling on Windows 2022-08-09 17:29:42 +02:00
69d45fea44 Use a GQueue for thumbnailing
It's mildly less awkward to use, and fixes one complexity issue.
2022-08-09 16:22:27 +02:00
4697a56760 Use cleaner paths when looking up thumbnails 2022-08-09 13:55:17 +02:00
4e11970a7e Do produce thumbnails of thumbnails, but in memory 2022-08-09 13:22:11 +02:00
ae0b5506ab Remove a stale comment 2022-08-09 08:31:51 +02:00
701846ab39 Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.

This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles.  Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.

(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.

We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)

There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.

Similarly, there is no intention to make the VFS writeable.

The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.

Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-08-08 18:06:50 +02:00
4927c8c692 Don't crash on orphan URIs 2022-08-08 15:14:20 +02:00
e6341e59bb Make Information easier to control from keyboard 2022-08-06 07:49:01 +02:00
33fb047a73 Make Alt+Return work in the browser as well 2022-08-06 07:49:01 +02:00
215141856a Add a mnemonic to the Information menu item 2022-08-06 07:49:01 +02:00
086dd66aa9 Add the information dialog to context menus
Images don't need to be open for ExifTool to work.

This also enables inspecting unsupported files, such as video.
2022-08-05 11:38:12 +02:00
8c6fe0ad32 Integrate dconf-editor
This is a more than adequate solution for now.
2022-08-05 09:58:04 +02:00
ca51c9413b Show parse names in Information dialog subtitles 2022-08-05 08:57:55 +02:00
857917aa92 Make file information retrieval asynchronous
Also, make error output scrollable.
2022-08-05 08:03:37 +02:00
51dc56c9df Improve support for opening HTTP URIs
While all GVfs files implement the mountable interface,
mounting may not actually achieve anything.
2022-08-05 04:15:42 +02:00
bb669743b6 Fix default filenames in the "Save as" dialog
- Don't assume the filesystem is in UTF-8.
 - Don't try to extract basenames directly from URIs.
2022-08-05 04:15:37 +02:00
d590d1da46 Add support for copying to clipboard 2022-08-04 00:35:22 +02:00
d1d9caaa5e Capitalize modifier names, prefer Command on macOS
So far, the macOS special casing is only partial.

Also, GtkShortcutsWindow confusingly labels Command as Meta.
2022-08-04 00:34:05 +02:00
5bae7c1bd2 Use gdk_event_triggers_context_menu() 2022-08-03 21:37:45 +02:00
6f83d1dceb Minor improvements for the user guide 2022-08-01 02:38:30 +02:00
ab70b30053 Centre ultra-wide items vertically
Overall, this looks better, even though we lose a baseline of sorts.
2022-07-31 04:35:04 +02:00
ea75579b33 Fix a crash with empty exiftool stderr output 2022-07-31 03:54:53 +02:00
8437164dec Try a bit harder to get a file's local path 2022-07-31 02:52:20 +02:00
fec64d5595 Support file information for non-local files 2022-07-31 01:14:42 +02:00
fefb4c16ac Support file information for FUSE-mounted paths 2022-07-31 00:12:53 +02:00
6baf1a7bbd Make the switch-to-browser button select last file
Before, it was only possible to achieve the same result using keyboard.
2022-07-26 00:31:43 +02:00
78636fdc18 Add sidebar/toolbar toggles to GSettings 2022-07-25 23:20:30 +02:00
e18f729488 Add thumbnail size to GSettings 2022-07-25 21:09:15 +02:00
9f1041988d Add a dark theme toggle to GSettings 2022-07-25 20:27:36 +02:00
fa034a1a6a Handle gdk-pixbuf's dynamic format support better
If we use it, install an update script.
2022-07-25 19:05:37 +02:00
dcc5b6c719 Use GSettings for a new native window toggle
Also, redo desktop files handling.
2022-07-25 19:05:27 +02:00
eca319e5e4 Extend a comment 2022-07-24 22:46:03 +02:00
817f1b6000 Support colour management on Windows
There is also an alternative WcsGetDefaultColorProfile() path
that might be necessary on some broken versions of Microsoft Windows,
which I certainly do not want to support.
2022-07-23 22:59:29 +02:00
31f428f4ec Make the window assume a centred position on macOS
Windows and Linux applications are more likely to not bother,
and their desktop environments don't place windows right in the corner,
which is what happens with GTK+/macOS.
2022-07-23 21:26:24 +02:00
4b4e24e71a Work around broken Cairo Quartz backend on macOS
Pre-render the padded pattern, costing us 2 megabytes of memory there.
2022-07-23 20:47:57 +02:00
4e84d6a802 Don't eat application launch errors 2022-07-23 17:29:18 +02:00
f94171fcf2 Make the jpeg-quantsmooth wrap work on Debian
Sadly, it's not possible to delete files using patch_directory.
2022-07-23 15:23:44 +02:00
4131a926f2 Fix the remaining Windows build error
Linux has st_mtim (and an st_mtime macro),
macOS has st_mtimespec (and an st_mtime macro),
Windows has just st_mtime.

GFileInfo would be another option, though it seems unnecessary.
2022-07-23 10:39:53 +02:00
4fcc506d84 Update README.adoc 2022-07-22 20:49:54 +02:00
5529727137 Fix thumbnail passing on Windows
LF was converted to CR LF, systematically corrupting bitmap data.
2022-07-22 20:49:54 +02:00
e137afa736 Fix a function name conflict on Mingw-w64
This could also be resolved through `#define NO_OLDNAMES`,
however the function rather deserved a more precise name.
2022-07-22 18:33:55 +02:00
891420edfd Handle back/forward mouse buttons on Win32/macOS
There is no conflict with X11/Wayland, because 4/5 are the scroll wheel,
which never gets forwarded to button-press-event.
2022-07-22 16:27:43 +02:00
0bfd3ad4ce Fix touch screen scrolling on sidebar breadcrumbs
The drag gesture needs to be disabled there,
because touch drags fail in an unfortunate way.
2022-07-22 15:40:17 +02:00
e03bc36f63 Print errors from launching new instances 2022-07-21 16:27:48 +02:00
d3b34cd482 Only offer horizontal browser scrolling if useful
Motivated by small screens.
2022-07-21 16:04:51 +02:00
b067c1948b Use GDK event handling return value constants 2022-07-21 16:03:36 +02:00
390e21a72d Fix touch screen drag scrolling in the browser 2022-07-21 15:33:42 +02:00
876fda4f55 Handle the long press gesture on browser items
Unfortunately, this doesn't work on X11, though Wayland seems fine.
2022-07-21 12:28:01 +02:00
60b2395940 Mildly improve Windows portability 2022-07-21 09:57:00 +02:00
e7c75f8f9b Remove forgotten include directive 2022-07-20 17:51:00 +02:00
a9a9d69a92 Add missing array sentinel value 2022-07-19 16:08:53 +02:00
26dead7ea4 Fix the About dialog animation on macOS 2022-07-17 16:05:08 +02:00
94f6938b9a Add a key binding for keeping the zoom/position 2022-07-17 15:26:20 +02:00
62b1e83541 Support horizontal scrolling in the browser
An unlikely situation.
2022-07-17 13:47:56 +02:00
bd2e929b77 Add ability to keep zoom/position when browsing 2022-07-17 13:04:29 +02:00
9a0647fdfd Improve the workaround for native GdkWindows
Overshooting caused the image to be one pixel taller/wider,
due to using ceil() within get_display_dimensions().
2022-07-17 11:45:02 +02:00
47b7600f5e Work around a mysterious no-image zoom issue 2022-07-17 09:57:28 +02:00
8f98c623ee Center zoom around pointer or middle of the view 2022-07-17 09:13:12 +02:00
4efda5347c Let FivView take care of its drag gesture
Making the GtkScrolledWindow's scrollbars draggable again.
2022-07-17 07:16:41 +02:00
23429d9631 Implement GtkScrollable in FivView
This fixes rendering and positioning behaviour when dragging on X11,
where we aim to use a native GdkWindow.
2022-07-17 05:27:06 +02:00
ca57c2632a Simplify view dragging code a bit 2022-07-16 14:57:44 +02:00
c55500f51a Support dragging the view
It would also be possible to handle this through press/motion/release
event handlers, though GtkGestureDrag is more convenient for hacking in
support for dragging to widgets not supporting GtkScrollable (yet).

There may be some undesired interactions lurking, besides the jarring
movements when dragging native GdkWindows (these are a pain).
2022-07-15 14:00:31 +02:00
1fee920902 Make the browser scroll with touchpad on Wayland
And generally clear up scroll handling.
2022-07-15 07:35:33 +02:00
c6096d05b5 Discard the inner sidebar's size request
It used to create a hole when there weren't enough bookmarks
to fill that space.
2022-07-14 12:47:35 +02:00
07aa11d78d Use GPatternSpec rather than fnmatch()
Fixing a portability issue on Windows, where we still aim to use
the shared-mime-info database.
2022-07-14 10:08:15 +02:00
de27dce09c Add a context menu to breadcrumbs 2022-07-04 20:44:48 +02:00
a1b2225750 Move the browser's popup menu to its own file 2022-07-04 20:44:47 +02:00
b87a109d61 Decode bitmap thumbnails through LibRaw as well 2022-06-10 22:47:00 +02:00
81145064de Generate TIFF structs/enums from a text file
This is to make the tables much easier to maintain.
2022-06-10 02:18:14 +02:00
60a8ee7a80 Build tools with Meson as well 2022-06-10 02:08:56 +02:00
84f8c9436f Downscale embedded thumbnails within minions
Otherwise the UI would become unresponsive during loading.
2022-06-08 02:51:55 +02:00
a8f7532abd Employ embedded thumbnail extraction
And store all direct thumbnailer output in the browser's cache--
low-quality thumbnails will always be regenerated, as is desired,
and we'll reload faster on devices where we don't store thumbnails.

This change improves latency at the cost of overall efficiency,
seeing as images with thumbnails will be spent cycles on twice.

Keeping this out-of-process avoids undesired lock-ups.
Moreover, embedded thumbnails can be fairly expensive to decode.
2022-06-08 02:51:54 +02:00
8dfbd0dee2 Add a command line option to extract thumbnails
Only use LibRaw for now, which probably has the most impact
using the least amount of effort.
2022-06-08 02:51:54 +02:00
930744e165 Add flags to the serialization protocol
It still needs no versioning, as it's not really used by anyone.

An alternative method of passing a "low-quality" flag would be
perusing fiv_thumbnail_key_lq from fiv-thumbnail.c, which would
create a circular dependency, unless fiv_io_{de,}serialize*()
were moved to fiv-thumbnail.c.
2022-06-08 02:51:54 +02:00
4ca8825e02 Clean up
Use gchar when memory is allocated through GLib.
2022-06-05 13:30:53 +02:00
024b5117b4 Get rid of our spng dependency
Thumbnails can be properly loaded using Wuffs now.
2022-06-04 23:14:15 +02:00
ac6b606ccc Bump Wuffs, support partial PNGs through it 2022-06-04 19:19:16 +02:00
8bba456b14 Cache thumbnails across reloads
This will speed up sort changes, as well as simple reloads,
at the cost of an extra hash map from URIs to Cairo surface references.

It seems unnecessary to provide an explicit option to flush this cache,
as it may be cleared by changing either the directory or the current
thumbnail size.
2022-06-04 16:37:25 +02:00
bb97445a96 Attach mtime to the browser's rescaled thumbnails 2022-06-04 16:37:25 +02:00
e2adac72cc Use the model's mtime for validating thumbnails
Saves a syscall, generalizes fiv_thumbnail_lookup(),
wastes a tiny bit of memory per entry.
2022-06-04 16:37:25 +02:00
3ddb0cf205 Expose the mtime of the model's entries 2022-06-04 14:50:56 +02:00
efc13db66e Plug two memory leaks 2022-06-04 01:28:44 +02:00
b6315482b7 Fix sort changes taking way too much time
All thumbnails were reloaded five times on each change.

GTK+/GObject's behaviour doesn't make a lot of sense, but such is life.
2022-06-02 11:46:16 +02:00
51ea785d83 Bump spng wrap 2022-04-20 03:03:30 +02:00
5c34a6846a Fix loading huge JPEGs
They fell back to gdk-pixbuf, then misrendered in the thumbnailer,
and crashed the program when loaded directly.

The second best we can do is scale them down, right after tiling,
which is a complex feature to add.
2022-03-09 18:04:36 +01:00
da507edd05 Prevent thumbnailing from disrupting mouse clicks 2022-02-24 21:52:25 +01:00
580b68789b Turn the browser into a DnD source
The destination does all the work of handling file operations.

Also, add some missing logic for horizontal scrolling.
2022-02-22 15:44:38 +01:00
31f9feab7b Use the X-GNOME-FullName desktop file key 2022-02-21 22:29:14 +01:00
41bd25e711 Avoid g_app_info_should_show() in context menus
We were hiding our own JPEG cropper.
2022-02-21 21:49:44 +01:00
d9435c988c Clean up 2022-02-21 21:20:16 +01:00
919a55c90b Try to thumbnail everything we can 2022-02-21 00:02:15 +01:00
68bb695054 Clean up 2022-02-20 21:29:34 +01:00
04ec292caf Make thumbnailers pass back raw images 2022-02-20 21:14:33 +01:00
a28fbf25bc Implement wide thumbnail cache invalidation 2022-02-20 15:44:42 +01:00
6c748439ed Use natural sort order
This is exposed in GLib through collate key construction.
2022-02-20 12:07:41 +01:00
fbf26a7d66 Show application icons in context menu items 2022-02-20 12:07:40 +01:00
1a8c461af2 Simplify sidebar DnD 2022-02-19 23:13:35 +01:00
2a73e46315 fiv-jpegcrop: avoid negatively sized crop regions 2022-02-19 23:04:12 +01:00
a68a73cf5c Don't mention gdk-pixbuf as a direct dependency
It's a similar case to Cairo.
2022-02-19 22:02:22 +01:00
433ede4bf1 Add a lossless JPEG cropper
This is more of an MVP, as metadata probably need adjustments.
2022-02-19 20:48:38 +01:00
3ae8be8348 Add a TODO comment
WebP can't save all JPEGs, because WEBP_MAX_DIMENSION is only 16383.
2022-02-18 19:54:05 +01:00
5d019e20b5 Make the view a drop target 2022-02-17 10:57:05 +01:00
03d1798e23 Add a missing header file include 2022-02-14 06:55:37 +01:00
ef2544868d Open items on mouse button release, not press
At least the left and middle mouse buttons seem to behave similarly
in other programs and systems.

Context menus are opened on button release on Windows and with some
GTK+ widgets (popovers in GtkPlacesSidebar).
2022-02-14 02:10:25 +01:00
0857a04a3a Scroll to selection when returning from the viewer 2022-02-13 13:18:36 +01:00
4302ec71f2 Make changing the browser zoom launch thumbnailers 2022-02-13 13:18:36 +01:00
ee5f63e50b Adjust keyboard shortcuts 2022-01-26 04:44:00 +01:00
6e26dc13b4 Only show the info bar when appropriate
The late, global gtk_widget_show_all() made it always start visible,
in particular when the program was launched directly on an image file,
and not in browsing mode.
2022-01-25 06:15:05 +01:00
5c725d1968 Fix some user guide formatting 2022-01-25 05:59:05 +01:00
ee71fb0dd0 Start a basic user guide
Move some information out there from the README.
2022-01-25 05:54:00 +01:00
381e5f57c7 Add TGA to the list of supported media types 2022-01-25 05:16:31 +01:00
788485d81e Redirect warnings to the info bar
And speed up thumbnailing of animated images while at it.

Also, fix thumbnailing SVGs with external links.
2022-01-24 05:48:13 +01:00
991e74b99b Redirect image open failure messages
Pop-up dialogs are quite annoying, as is not being able to
iterate over broken images.

This will also be useful for warnings and asynchronous loading.
2022-01-24 02:48:38 +01:00
38670428da Add keyboard shortcuts for thumbnail size 2022-01-23 06:44:50 +01:00
a7e638207f Fix Meson
The disabler, for some reason, bubbles up to its target.
2022-01-23 04:44:41 +01:00
098895bfd9 Remove SVG debugging tools
I already know how librsvg over Cairo behaves.
2022-01-23 04:15:48 +01:00
235b14dc11 Fix a case in orientation mirroring 2022-01-23 04:15:48 +01:00
6ce5c7c2b6 Scale SVGs accurately in the viewing widget 2022-01-23 04:15:48 +01:00
07e7d39ea2 Produce properly scaled SVG thumbnails 2022-01-23 04:12:11 +01:00
562e140a1e Add backend for accurate SVG scaling 2022-01-23 04:12:10 +01:00
b71d5dff57 Make truncated WebP parts always transparent 2022-01-23 00:27:20 +01:00
c85de6b20f Update a comment 2022-01-22 22:51:59 +01:00
3796f56e81 Load even partial WebP images 2022-01-22 22:45:35 +01:00
0a11abd3fe Reorder code 2022-01-22 21:38:00 +01:00
78faf438a5 Improve WebP error reporting 2022-01-22 21:31:16 +01:00
f2eb7621b4 Bump Wuffs, add TGA support through it 2022-01-22 21:02:56 +01:00
8877e17108 Default to gdk-pixbuf even for Wuffs formats 2022-01-22 21:02:56 +01:00
686d45553b Plug a corner-case memory leak, fix range checks 2022-01-21 19:03:05 +01:00
4ba1d85363 Add preliminary support for resvg
It claims better SVG support, but it sucks for a plethora of reasons.
2022-01-21 09:14:19 +01:00
45238d78cd Mesonize JPEG Quant Smooth
Now SIMD works on amd64, although the build remains questionable,
because it assumes that all of its compiler flags will work.

This way we lose an uncomfortable git submodule.

Also, add Meson subprojects to .gitignore.
2022-01-19 01:11:47 +01:00
8a656121a3 Update command line usage string 2022-01-16 01:51:04 +01:00
6a1278786c Fix a compiler warning 2022-01-13 23:42:35 +01:00
13ae4810ca Apply some bits of GNOME HIG 1.0 2022-01-13 21:38:32 +01:00
222ba6a060 Add a comment about HDR PNG 2022-01-13 21:09:16 +01:00
8e8cf49343 Add a normally hidden, stubbed-out menu bar
So that the About dialog is discoverable now at all.
2022-01-13 21:08:59 +01:00
6de5ab6298 Select the file when Return-ing from the viewer
It might make sense to also do it on M-Left or the back button.
Not sure about it so far.
2022-01-12 11:15:35 +01:00
757bc9beaa Fully support GNOME's inode/directory mechanism 2022-01-12 11:12:32 +01:00
f632510d2a Put reloading the image as its own action 2022-01-12 10:58:33 +01:00
05453718bb Avoid blank browser space when resizing the window 2022-01-12 10:41:33 +01:00
cfa90fb7de Don't hardcode the project name in its URI 2022-01-12 10:41:33 +01:00
f1e9e47e13 Implement browser keyboard navigation 2022-01-11 14:36:19 +01:00
764312652d Support invoking a context menu from keyboard 2022-01-11 12:26:28 +01:00
e663f02754 Implement selection in the browser
Keyboard controls are missing so far.
2022-01-11 11:27:35 +01:00
1a190001fc Make the browser implement its own scrollable
It's quite rough around the edges so far.
2022-01-10 11:53:15 +01:00
3bf41993a3 Clean up the About animation 2022-01-10 10:25:08 +01:00
4f19a67da3 Add an unnecessarily fancy about dialog 2022-01-09 19:11:36 +01:00
12e3c42888 Move key bindings to a more usual shortcut
gThumb is confused.
2022-01-09 12:31:19 +01:00
146c5c6977 Improve the icon
Now it's simple, colourful, pixel-aligned, balanced and everything.

The ligature is a bit awkward, but it will do.
2022-01-09 09:47:44 +01:00
25dcc3b136 Handle view bindings through an action signal
This makes them adjustable.
2022-01-09 07:48:44 +01:00
09e5a02ed6 Swap zoom in/out action buttons 2022-01-09 05:24:57 +01:00
af80303719 Update mildly confused comments 2022-01-08 12:03:52 +01:00
d65e83a21d Fix a key binding conflict 2022-01-08 10:05:32 +01:00
77de7efc55 Bump Wuffs, clean up image loading 2022-01-08 08:04:58 +01:00
35c1f2c8ba Parallelize thumbnail production 2022-01-08 07:46:28 +01:00
b973d323ba Fix GIF decoding for certain files
The handling is not perfect yet, but it should be fine enough.
2022-01-08 05:34:01 +01:00
231b77e6c0 Make sort order changes update iteration order 2022-01-07 16:30:47 +01:00
7160a915e2 Update README 2022-01-07 12:16:19 +01:00
6a878fd3c4 Bump more copyright years 2022-01-07 09:53:58 +01:00
3274b64f5a Fix SVG thumbnailing
They're not loaded as image surfaces.
2022-01-07 09:43:28 +01:00
feda4fd70f Don't force sanitizers in debug builds 2022-01-07 08:37:18 +01:00
59af3b7e7b Show sidebar DnD targets when dragging breadcrumbs 2022-01-07 07:41:05 +01:00
fc559c3d01 Work around an annoying GTK+ issue 2022-01-07 07:28:05 +01:00
6869816cc4 Fix desktop file regeneration with certain AWKs 2022-01-07 06:47:13 +01:00
8fdf9e2bc3 Turn breadcrumbs into DnD sources
Now it makes sense to keep that GtkSidebar placeholder item around.
2022-01-07 06:11:42 +01:00
235af37382 Handle middle mouse clicks on breadcrumbs
Also, don't act on button releases happening outside the widget.
2022-01-07 05:33:33 +01:00
06ab13797d Add some padding to the location dialog
It looked awful with Ubuntu's theme.
2022-01-07 03:48:30 +01:00
e70bb20934 Improve toolbar hiding 2022-01-07 03:34:32 +01:00
b0de0e09bd Add a screenshot to the README 2022-01-06 12:43:27 +01:00
077747f428 Improve desktop file regeneration 2022-01-06 11:44:01 +01:00
3ae22e49ee Make sure our info-symbolic is actually used
The previous placement was ultra-fallbacky, "info" was picked instead.
2022-01-06 10:13:10 +01:00
bb67df716c Mildly improve Ubuntu 20.04 compatibility 2022-01-06 08:39:33 +01:00
4cd2978e21 Add a keyboard shortcut for filtering 2022-01-06 06:59:30 +01:00
68e786b4e8 Fix build with JPEG-QS but without lcms2 2022-01-06 06:15:08 +01:00
9a1396b91f Update comments 2022-01-05 12:07:05 +01:00
5abf6a719f Add UI for sort order settings 2022-01-05 10:54:36 +01:00
0a6b06d1d0 Fix browsing the "resource" GVfs schema 2022-01-05 07:59:18 +01:00
d889acc315 Show available protocols in open location dialog 2022-01-05 07:59:18 +01:00
6142bf9c53 Automount from location input 2022-01-05 06:40:39 +01:00
244779bd8c Factor out make_browser_sidebar()
It's a very mild improvement, but an improvement nonetheless.
2022-01-05 05:17:18 +01:00
685defa684 Rename the whole project shorter
There is no point in claiming speed, it turns out to be a strange focus
to have, considering the amount of available innovations to make.

The new name does not appear to be taken by anything important.
2022-01-05 04:45:46 +01:00
b935b0baf8 Use a unified filesystem model
This removes some duplication of effort.

So far, sorting adjustments are not exposed in the UI.
2022-01-05 03:48:22 +01:00
2ac918b7ab A bunch of additional fixes 2021-12-31 03:43:51 +01:00
5f8dc88fa7 Minor URL-related fix-ups 2021-12-31 00:41:04 +01:00
380ddd540b Convert all loading to use GFile
Now we're able to make use of GVfs, with some caveats.
2021-12-30 22:32:29 +01:00
8b232dc444 Add pointless likelihood macros 2021-12-30 21:49:00 +01:00
fa69935e67 Document our thumbnails 2021-12-29 21:17:30 +01:00
4832474c5f Partially colour-manage TIFFs 2021-12-29 02:46:40 +01:00
0110e0a5d2 Check wide thumbnail metadata 2021-12-29 01:55:05 +01:00
c49e58a0ba Fix thumbnailing cancellation
Everything's according to GAsyncResult documentation.
2021-12-28 23:49:35 +01:00
98bdab443a Hardcode Exif orientation in thumbnails 2021-12-28 23:29:58 +01:00
bf47782f0a Move thumbnails into their own source file
And clean up identifiers.
2021-12-28 20:18:25 +01:00
c1af556751 Load back wide thumbnail metadata 2021-12-28 19:59:31 +01:00
2d86ffed34 Save thumbnails lossily, with metadata 2021-12-28 18:54:27 +01:00
aaa7cb93c3 Fix transparent gdk-pixbuf loading 2021-12-28 02:07:42 +01:00
d4b51f07b5 Avoid unused alpha channels when rescaling 2021-12-28 02:07:42 +01:00
720464327c Clean up 2021-12-28 02:07:42 +01:00
ad1ff06aff Avoid producing thumbnails of thumbnails 2021-12-28 02:07:42 +01:00
af2eb411d9 Try to regenerate low quality thumbnails 2021-12-28 02:07:42 +01:00
004919cbc5 Clean up
This makes the thumbnailer able to load at most one directory,
which we don't particularly mind.
2021-12-28 00:37:55 +01:00
336053f24d Implement trivial wide thumbnail production
Also make libwebp a required dependency.
2021-12-27 21:51:01 +01:00
2f993502fc Make use of gAMA and sRGB PNG chunks
Neither EoG nor gThumb handle this correctly.
2021-12-26 03:25:38 +01:00
e5b1a1861c Avoid double CM in saved WebPs 2021-12-26 02:02:57 +01:00
e37acf365a Colour manage all WebP forms
It could be done better, but at least it works at all.
2021-12-26 01:14:27 +01:00
ccf15bc8ae Almost fully colour-managed Wuffs (BMP, GIF, PNG) 2021-12-25 21:53:39 +01:00
5e4476ff71 Improve gdk-pixbuf colour management 2021-12-25 19:15:54 +01:00
035997750e Add a few keyboard shortcuts 2021-12-25 18:28:37 +01:00
7a4b5cd065 Colour manage opaque, up to 8-bit images 2021-12-25 18:28:37 +01:00
40c1f8327e Use Little CMS for JPEG colour management 2021-12-24 14:19:22 +01:00
6419209c98 Avoid enhancing just opened images 2021-12-22 14:26:53 +01:00
2d4cab52b3 Integrate jpeg-quantsmooth
Also, don't pointlessly store JPEGs in an ARGB Cairo surface.
2021-12-22 14:20:39 +01:00
46edd4406c Make file information fields selectable
Get rid of useless GtkTreeView.
2021-12-22 08:38:19 +01:00
b35590a51c Temporarily put file information in GtkTreeView
It's aligned and prettier than the label before, but it sucks ass.
2021-12-21 19:27:09 +01:00
9899a26635 Add a file information dialog based on ExifTool
Right now, it isn't very pleasing to use.
2021-12-21 13:05:11 +01:00
24f9d21ca7 Clean up
Get rid of undesired indentation.
2021-12-21 08:43:47 +01:00
ad29013e44 Add zooming to fit width/height if larger
Also, mildly refactor get_surface_dimensions().
2021-12-21 08:13:31 +01:00
33851295d8 Bind M-Home to going to the home directory 2021-12-21 07:20:06 +01:00
46f90f2f35 Improve the "Save as" dialog, clean up 2021-12-21 06:31:52 +01:00
9ba3679e89 Make use of GtkShortcutsWindow 2021-12-20 16:04:50 +01:00
6a61d01f4d Bind hiding the toolbar 2021-12-20 12:15:52 +01:00
f435252492 Add a checkerboard toggle 2021-12-20 11:53:17 +01:00
dfa1fed18b Add a context menu to opened directories
So that they can be opened with, e.g., Thunar.
2021-12-20 10:15:46 +01:00
58d11ebbff Make M-Up go to the parent directory
As in Windows Explorer and other software.
2021-12-20 09:58:41 +01:00
1221325b3e Stop forcing a dark theme variant
And make it so that both Adwaita variants look okay.
2021-12-20 07:22:24 +01:00
c3eb5ca170 Simplify toolbar separators 2021-12-20 05:34:12 +01:00
9c0e9d8d49 Stretch the toolbar across the window
The division is kind of logical, it might make sense for the view
to create the center widget, then we'd get rid of some recently added
GObject boilerplate.

Only make_separator() is kind of annoying.
2021-12-20 05:33:27 +01:00
ada67f044a Optimize thumbnail rendering
Massive responsivity gains have been achieved here.

Rescaling performance doesn't seem to be particularly affected.
2021-12-20 04:40:35 +01:00
63955e881d Add a convenience dark theme variant toggle 2021-12-20 03:55:53 +01:00
6130f527d4 Mark a dead end 2021-12-20 03:49:35 +01:00
3da1d32df7 Make Exif orientation work with SVG
SVG doesn't contain Exif, but this is how we handle rotation/mirroring.
2021-12-19 12:21:14 +01:00
56d623fe52 Make C-r also reload the current directory 2021-12-19 12:21:14 +01:00
2c46ca262b Add directory history
Not fully polished yet (see FIXME), but it's a start.
2021-12-19 10:59:11 +01:00
39cd52905b Control TOOLBAR_FILE_{PREVIOUS,NEXT} sensitivity 2021-12-19 10:14:08 +01:00
9feb53a792 Use the same iteration order in view/browser 2021-12-19 09:12:06 +01:00
4427da5343 Store the full path of the loaded image
Fixes a minor inconsistency with the window title.
2021-12-19 09:12:06 +01:00
92c6ca6c35 Give the zoom label a minimum width
This also hides a GTK+ bug.
2021-12-19 07:32:21 +01:00
ae57c45c2a Insensivitize inappropriate toolbar actions 2021-12-19 07:04:34 +01:00
bac92f2612 Update a comment 2021-12-19 06:14:43 +01:00
64ba54e8e6 Align tables 2021-12-19 06:08:47 +01:00
6e903f6f5c Add a playback toggle button 2021-12-19 05:37:11 +01:00
e23ed245db Add toolbar toggle buttons for scale-to-fit/filter 2021-12-19 04:43:47 +01:00
b78010ccb1 Adopt shorter identifiers
Also, slightly reformat the source code according to clang-format.
2021-12-18 07:04:01 +01:00
c136c089fa Add a GObject property for view filtering 2021-12-17 07:39:12 +01:00
1c2a441cb5 Add a simple toolbar to the view
There is still considerable work to be done.
2021-12-17 07:01:37 +01:00
0b6b3d8290 Improve light theme compatibility
@content_view_bg has been there since ever.

Many colours remain hardcoded, but it's a major improvement.
2021-12-16 04:39:38 +01:00
c3a098c503 Add very basic XMP support 2021-12-16 00:29:12 +01:00
577f8c0d92 Fix inconsistent indentation
VIM has a stupid default configuration for this file.
2021-12-15 06:48:17 +01:00
4d8165d790 Add some WebP notes 2021-12-15 06:15:41 +01:00
c18404efee Add basic print functionality 2021-12-15 04:44:34 +01:00
bff2b92c9e Clean up 2021-12-15 03:53:13 +01:00
7297c40f93 Employ libwebp's alpha premultiplication
It seems to perform roughly equally in optimized builds.
2021-12-15 03:43:57 +01:00
ea2d159773 Clean up dependencies 2021-12-15 03:29:47 +01:00
4f035bc6b1 Allow saving the current frame/page in WebP
Also support saving just the metadata.
2021-12-15 02:45:20 +01:00
18f7607e1b Add a most basic tool to inspect ISO BMFF files
This can be massively extended.
2021-12-14 01:22:51 +01:00
1478a9f83f Add a tool to extract information from WebP 2021-12-13 19:05:23 +01:00
9eb9cc44aa Clean up 2021-12-13 19:05:23 +01:00
e161f77359 Recognize a few more tactical TIFF tags 2021-12-13 02:12:41 +01:00
3ed23e423b Add pedantic WebP dimensions overflow checking 2021-12-12 23:39:36 +01:00
6c7d431e35 Finish WebP support with animations 2021-12-12 23:35:31 +01:00
006e547deb Read out Exif and ICC profiles from WebP 2021-12-12 21:31:30 +01:00
caca14036c Add preliminary direct support for WebP 2021-12-12 19:15:34 +01:00
b868e76a15 Ignore libjpeg-turbo warnings 2021-12-12 01:27:53 +01:00
2147dba8c4 Add a comment about TIFF/EP vs libtiff 2021-12-12 00:22:21 +01:00
121c63e35e Add a basic tiffinfo utility
Also fix a few TIFF-related issues.
2021-12-12 00:22:20 +01:00
1bd5cb02e7 Extract HEIF auxiliary subimages 2021-12-11 17:09:39 +01:00
1cbd0141a1 Clean up 2021-12-11 17:09:39 +01:00
7e92011ab2 Extract the ICC profile and Exif data from HEIC 2021-12-11 16:11:26 +01:00
ac70c7724b Add preliminary HEIF/AVIF support
The gdk-pixbuf plugin does not work here, for whatever reason.

Moreover, close integration exposes higher bit depths, metadata,
and auxiliary images.

The library is awful and copylefted, but it's the only reasonable
thing that works.
2021-12-11 16:11:25 +01:00
5f4090aaee pnginfo: extract some ImageMagick profiles 2021-12-10 19:08:37 +01:00
fa15707d9b pnginfo: extract eXIf chunk data 2021-12-10 17:23:26 +01:00
16c6766e79 jpeginfo: update comment 2021-12-07 10:21:21 +01:00
3fe2f60a19 jpeginfo: clean up MPF 2021-12-06 20:54:25 +01:00
a519a5dec6 jpeginfo: describe Photoshop records 2021-12-06 19:00:00 +01:00
4b306b7c93 Don't crash the view when no image is present 2021-12-06 15:29:31 +01:00
8e2958051d jpeginfo: mostly finish Exif decoding
Diminishing returns and all.
2021-12-05 16:11:41 +01:00
1ae803a62e jpeginfo: decode the main Exif subIFD 2021-12-05 14:06:14 +01:00
55d8fdebf1 jpeginfo: review and update TIFF 2021-12-05 11:54:11 +01:00
e2bdda77a3 jpeginfo: decode MPF MPEntry 2021-12-05 10:23:25 +01:00
bac9cd24fc jpeginfo: clean up
No more -Wunused-function warnings.
2021-12-04 10:32:32 +01:00
7cb2879c03 jpeginfo: trivially decode Multi-Picture Format 2021-12-04 09:34:14 +01:00
68009c1d3e jpeginfo: descend into Exif IFDs 2021-12-04 07:04:34 +01:00
5d659d208c jpeginfo: parse all numeric TIFF values 2021-12-04 06:52:25 +01:00
4d9236336c jpeginfo: parse TIFF UNDEFINED values 2021-12-04 06:52:25 +01:00
4cbf9239ee jpeginfo: decode more TIFF tags and values 2021-12-04 06:04:33 +01:00
64d2f902f2 jpeginfo: fix a typo from the TIFF 6.0 spec 2021-12-04 06:04:29 +01:00
15f57a079e jpeginfo: decode some TIFF/Exif values 2021-12-03 15:54:02 +01:00
06779c6bdd jpeginfo: decode basic TIFF tag names 2021-12-03 14:57:55 +01:00
46c46ac093 jpeginfo: clean up 2021-12-03 14:19:48 +01:00
38427ff88e jpeginfo: add a basic TIFF/Exif parser 2021-12-03 13:10:52 +01:00
24de9aee53 jpeginfo: multisegment Exif, rough PSIR 2021-12-03 10:49:14 +01:00
a31b08a2d1 RAW -> raw photos 2021-12-02 13:01:31 +01:00
7b53edd6af jpeginfo: parse out ICC profile name and version 2021-12-01 13:04:43 +01:00
9707b6a254 jpeginfo: human-friendly frame content description 2021-12-01 09:10:29 +01:00
5bcaf39b32 jpeginfo: fix a minor memory leak 2021-12-01 08:52:10 +01:00
9c77aac640 Add a tool to extract information from JPEG images 2021-12-01 08:38:13 +01:00
0d9cb78f03 Force sanitizers for debug builds 2021-11-30 22:53:01 +01:00
1db233648f Add more key bindings 2021-11-29 22:45:36 +01:00
cfe3dc55c6 Animate animations 2021-11-28 23:41:09 +01:00
33f24fa184 Update comments 2021-11-28 19:15:38 +01:00
6fc5d7a3d7 Improve Wuffs animation loading 2021-11-28 19:09:33 +01:00
d930b2b245 Get ICC profile and orientation from libtiff
Pain has been outsourced to someone from the past,
I just blindly trust the orientation mapping.
2021-11-28 03:39:36 +01:00
666bfc0759 Support using libtiff directly
Multiple directories are read as multiple pages.

The error handling is mildly questionable, as is libtiff.
2021-11-28 02:20:23 +01:00
f1742ec7da Fix an annoying double-unref 2021-11-28 01:58:58 +01:00
1ee975b110 Bump the spng wrap to 0.7.1
Removes an annoying warning.

Manual job, not in the wrap database yet.
2021-11-27 20:21:52 +01:00
c39ac1a9da Enable viewing all X11 cursor sizes 2021-11-27 18:59:05 +01:00
085f2d7eef Use GFile a bit more 2021-11-27 02:34:24 +01:00
b97ac26cfb Allow opening in a new window from the sidebar 2021-11-26 23:02:00 +01:00
bae65a61f7 Add an option to turn off filtering 2021-11-26 22:35:29 +01:00
174896d3e6 Nullify a concern 2021-11-26 22:28:43 +01:00
6c089eb1d2 Support CMYK JPEGs on big endian 2021-11-26 22:28:42 +01:00
18e96d8c9d Allow frame iteration in both directions 2021-11-26 20:54:41 +01:00
bd7f2f8c98 Handle Exif rotation
Does not currently work for SVG and X11 cursors.
2021-11-26 19:54:22 +01:00
8c89759325 Allow manual animation frame iteration 2021-11-26 17:14:51 +01:00
dd8461cebf Parse out Exif orientation 2021-11-26 17:00:36 +01:00
bafad1a67e Add a function to decode TIFF/Exif Orientation 2021-11-26 03:16:41 +01:00
a5f64b1a65 Extract ICC profiles from gdk-pixbuf 2021-11-26 00:46:19 +01:00
f151fcb72b Extract all frames from GIF/APNG animations
So far none of the surface userdata is used.
2021-11-25 16:56:42 +01:00
1d2f6243e0 Extract Exif and ICC profiles from Wuffs 2021-11-25 01:54:40 +01:00
2ea2178724 Read Exif and ICC profile metadata from JPEGs 2021-11-24 20:08:15 +01:00
c597e7bc2c Update README 2021-11-23 20:50:01 +01:00
1c40fa8adb Add an "Open With" context menu to browser items 2021-11-23 20:50:01 +01:00
fee901a590 Improve memory management 2021-11-23 17:13:21 +01:00
7ab1a6d246 Improve browser open handling 2021-11-23 00:38:14 +01:00
1e6689aed4 Mildly improve path autocompletion 2021-11-23 00:26:49 +01:00
e6ad6c6302 Update README 2021-11-22 20:52:12 +01:00
dd2a95bb99 Bump Wuffs 2021-11-22 20:40:48 +01:00
64fd216409 Fix opening files starting on dashes
fastiv is a bright exception to the sad rule now.
2021-11-22 20:40:48 +01:00
047e49051b Register for opening directories 2021-11-22 20:37:16 +01:00
4ed6aa6ad7 Don't claim to be able to open several files 2021-11-22 18:23:26 +01:00
8fed3f5a36 Add a better key binding for switching 2021-11-22 16:46:04 +01:00
8efd11d4e5 Update README 2021-11-22 16:35:57 +01:00
a3855e8f12 Add a tooltip to ellipsized directory labels 2021-11-22 16:34:50 +01:00
e239aca6f4 Make Ctrl-scrolling change thumbnail size 2021-11-22 15:33:36 +01:00
e663368ee4 Add filename tooltips to the browser 2021-11-22 15:19:24 +01:00
8070c7f9ee Make browser item spacing adjustable from CSS 2021-11-22 15:08:56 +01:00
0bec06b55d Fix further focus issues 2021-11-22 13:01:43 +01:00
97109b1e58 Fix browsing right after opening a file directly 2021-11-22 12:42:26 +01:00
a719147bf3 Another focus-related fix 2021-11-22 12:07:03 +01:00
cd72ea902f Fix two issues with browser scrolling 2021-11-22 01:44:57 +01:00
c4dead2eee Fix another mysterious GTK+ issue 2021-11-22 00:48:21 +01:00
a8796512d2 Improve the window title situation 2021-11-21 21:53:07 +01:00
8b1a14decb Bind double click to full screen toggle 2021-11-21 21:22:14 +01:00
d0fb24bf6b Use GDK button constants 2021-11-21 21:22:14 +01:00
2571bf15a9 Resolve key binding conflict
Toggle fullscreen vs. toggle scale to fit.
2021-11-21 21:07:51 +01:00
1c57eef05a Sort files in the browser as well 2021-11-21 21:07:51 +01:00
5fea2245f1 Remove insanity 2021-11-21 20:47:07 +01:00
2b17ed838a Add ability to use different thumbnail sizes 2021-11-21 20:19:25 +01:00
f4b727589b Update README
I've noticed people often desire editing capabilities,
which is an unreasonable expectation.
2021-11-21 15:45:35 +01:00
c77bccccb8 Implement filtering by supported extensions 2021-11-21 11:01:30 +01:00
6dd0414d0a Sort files and directories by name 2021-11-21 00:22:29 +01:00
8376ae9c4a Add some custom action buttons to the sidebar
So far they're inactive, and do not do anything.

Change the icon for the current directory to stand out.
2021-11-20 22:02:02 +01:00
5ebfebb8fc Make the browser grab focus when clicked 2021-11-20 18:46:38 +01:00
35500b8697 Readjust meson.build to jpakkane's brain damage 2021-11-20 14:35:17 +01:00
803f841463 Fix key handling and Meson 2021-11-20 14:28:32 +01:00
09547184c3 Fix an embarrassing crash on directory change 2021-11-20 13:18:31 +01:00
2b8350eceb Fix some issues with browser/view switching 2021-11-20 13:04:26 +01:00
75994cd85a Make a middle click open items in a new instance 2021-11-20 12:45:33 +01:00
3e9a388537 Load symbolic icons as a fallback
Now there are no missing items in the browsers.
2021-11-20 12:35:28 +01:00
6eb78f1a5c Make C-l interpret relative paths better
It is quite hard to make this work good.  At least it's not terrible.
2021-11-19 23:11:11 +01:00
216767d7ee Add a customized sidebar widget
Slowly eliminating all potential uses of GTK+'s standalone
file open dialog, which is highly duplicitous.
2021-11-19 20:03:43 +01:00
3bc07e00d9 Enable opening from sidebar in a new window 2021-11-18 22:08:45 +01:00
9e45ba249e Bind fullscreen switching
Also, move Tab/Enter bindings to the view's key press handler.
2021-11-18 22:08:45 +01:00
b23198f675 Try to use more screen real estate by default 2021-11-18 22:08:36 +01:00
411f0b3e91 Bind F5 and r to refreshing the directory 2021-11-18 13:58:46 +01:00
f8526d486a Do not lie as much in the desktop file 2021-11-18 13:41:58 +01:00
06af1a3cc9 Add a command line option to list supported types
Make it work without a display connection.
2021-11-18 12:46:05 +01:00
47293cfc10 Make the forward mouse button go back to the view
For symmetry.
2021-11-18 12:46:04 +01:00
d7a25ad894 Make the Open dialog useful
In the meantime.
2021-11-18 11:21:21 +01:00
0433c1a027 Add a sidebar with places
It happens to fix an issue with scroll offset resets in the browser.

Otherwise, it's very much WIP.
2021-11-18 10:37:47 +01:00
e045a35437 Fix loading of opaque GIFs 2021-11-18 10:37:47 +01:00
61225574d3 Actually fix adding to the list of recent files 2021-11-18 10:37:47 +01:00
db7a28b187 Add support for opening Xcursor files
Sadly, they don't have a canonical extension, and they don't show up
in the browser.  We might want to employ some level of sniffing.
The first 16 bytes are enough to identify a lot.
2021-11-17 13:49:15 +01:00
e8754f43a6 Fix zooming in through the keyboard 2021-11-17 08:38:45 +01:00
6eec8e7360 Fix adding images to the list of recent files 2021-11-16 14:57:02 +01:00
c4d58cb9ad Prefer the dark theme variant 2021-11-16 14:57:02 +01:00
9bebb0a3fe Make this work at all in macOS/Homebrew 2021-11-16 08:51:29 +01:00
11b7969459 Support opaque 16-bit images as RGB30 with Wuffs
Do not check whether the window's visual can make use of them,
since they're arguably rare enough.

With transparent images, we're limited by Cairo's formats.
2021-11-15 14:21:22 +01:00
e835c889a1 Don't use a side buffer to load thumbnails
Undoing part of a recent commit.
2021-11-15 10:30:02 +01:00
6f86911df6 Slightly optimize thumbnail loading
Now it translates to just x86 bswap and ror.
2021-11-15 09:47:00 +01:00
37adaac965 Let modified wheel events scroll the view 2021-11-14 03:37:09 +01:00
1dce2e079c Fix a typo 2021-11-14 03:37:08 +01:00
c905f64d12 Expose view settings as GObject properties 2021-11-14 02:48:11 +01:00
1f0d6b24d8 Cache the browser's GDK cursor object 2021-11-13 13:40:46 +01:00
7d972e9334 Add scaling to fit, make this the default 2021-11-13 12:47:10 +01:00
b8cc43eb91 Bind the mouse back button on the view 2021-11-13 10:05:05 +01:00
a1db89d91c Make scaling accessible from the keyboard 2021-11-13 09:51:16 +01:00
73dd5bf1a0 Improve key handling
Iteration should be limited to the view.

g_signal_connect_after() did not work as I hoped it would.
2021-11-13 09:41:37 +01:00
7dba21c6d8 Use the hand/pointer cursor in the browser
Also, fix the build.
2021-11-13 09:21:28 +01:00
d20c6469c0 Clean up 2021-11-13 09:06:01 +01:00
f7c1006053 Actually add gdk-pixbuf to dependencies 2021-11-12 13:42:59 +01:00
f74e7c34d5 Tell wrapped spng to give us a static library 2021-11-12 13:02:57 +01:00
3299cbf825 Parallelize thumbnail loading
GLib makes this easy.

They should all be local, and fast to access, so the CPU is the limit.
2021-11-12 12:22:36 +01:00
972bd80fa5 Add a Meson wrap for spng 2021-11-12 11:49:12 +01:00
21b110a7d6 Use spng to load thumbnails
Speed matters here, and this makes us about 20 percent faster
at loading large directories.

Moreover, libpng's PNG_ALPHA_BROKEN is indeed broken.

Thumbnails have a fairly fixed format, so there are very few practical
corner cases that could have been missed.
2021-11-12 11:45:34 +01:00
afc08df234 Fix GtkWidget::key-press-event callback prototype 2021-11-12 11:45:34 +01:00
062b5757da Update comments 2021-11-12 11:45:34 +01:00
f341c8f8c3 Make the 1 key reset the zoom 2021-11-11 22:59:31 +01:00
cfd2e5d9a5 README.adoc: fix dependency list 2021-11-11 22:51:59 +01:00
192698b7bd Add support for defaulting to gdk-pixbuf 2021-11-10 21:34:15 +01:00
405f975899 Use a checkerboard pattern on item background 2021-11-10 21:06:43 +01:00
fc4eb97218 Improve browser item rendition, use CSS
It's not fully hardcoded anymore, and the border is better adjustable.

Item spacing and the fade constant can't be /meaningfully/ put in CSS.
2021-11-10 21:06:43 +01:00
77f0e142c2 Reflect that new list of files changes the layout 2021-11-10 03:20:41 +01:00
e9d0325c62 Add images to the list of recent files 2021-11-10 03:20:41 +01:00
0cda41732f Add C-n to open a new instance in the directory
Also, improve error messages when opening a file fails.
2021-11-10 03:20:40 +01:00
ffda836a15 Also render SVGs with unspecified dimensions 2021-11-10 01:04:00 +01:00
7ef4a06def Improve librsvg integration
Let it load external <image>s and not rescale images.
2021-11-10 00:46:19 +01:00
1c5cc50939 Add very basic SVG support
We need to refactor, so that SVGs are pre-rendered on each change
of scaling by librsvg directly, because some elements may be rasterized.

It would be best to also support building against resvg.
2021-11-10 00:23:19 +01:00
1e380f695a Fix obsolete header includes 2021-11-09 18:15:41 +01:00
ed39a9b434 Add elementary scrolling support to the view 2021-11-09 06:57:02 +01:00
a135d6f332 Enable opening images from the browser
Also, make it possible to go back, in a roughly implemented manner.
2021-11-09 06:03:02 +01:00
a0408abdf2 Don't render rows needlessly 2021-11-09 04:14:19 +01:00
4361fdd1be Clean up 2021-11-09 03:48:36 +01:00
527a081f54 Add glowing borders around browser items
The styling is mostly hardcoded for now, need to figure it out.
2021-11-09 02:48:40 +01:00
155f57db20 meson.build: fix the io-benchmark target 2021-11-08 17:25:57 +01:00
7d640651cb Set a default window size 2021-11-08 08:00:48 +01:00
d2ef5c9c95 Pre-layout the browser
Now the widget is scrollable.
2021-11-06 23:56:44 +01:00
a346ff8d02 Don't needlessly call setlocale()
gtk_init_with_args() does it for us.
2021-11-04 20:19:30 +01:00
9045898fb6 Don't rescale thumbnails in sRGB
pixman is too slow at this, maybe do it later, and optionally.
2021-11-04 19:52:14 +01:00
45df774cc9 Fix scaling in the view, as in the browser
The source pattern needs to be padded.
2021-11-04 19:42:22 +01:00
cdb8d852a6 Pre-scale loaded thumbnails, and only when needed 2021-11-03 14:15:34 +01:00
dbc500ae9f Improve thumbnail scaling and alignment
Stretch thumbnails by up to half a pixel so that they align nicely.

Make use of pixman's sRGB mode.
2021-11-03 14:03:28 +01:00
d6ac386dbd Bump wuffs
No great differences.
2021-11-02 04:53:33 +01:00
ce0500ef5c Clean up and fix key handling
Arrow keys now work.
2021-11-01 07:20:25 +01:00
e3b8fc9599 Update README 2021-11-01 05:17:27 +01:00
c8df325c70 Split out xdg.{c,h} 2021-11-01 05:17:26 +01:00
6eecee6b91 Incorporate most clang-format changes 2021-11-01 05:17:26 +01:00
810a1fee57 Add clang-format configuration 2021-11-01 05:17:26 +01:00
ab283d3988 Split out fastiv-io.h, move media types list 2021-11-01 05:17:26 +01:00
7ca53b031e tools: fix the Makefile 2021-11-01 05:17:25 +01:00
83 changed files with 21566 additions and 1781 deletions

12
.clang-format Normal file
View File

@@ -0,0 +1,12 @@
BasedOnStyle: LLVM
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
AlwaysBreakAfterReturnType: AllDefinitions
BreakBeforeBraces: Linux
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
SpacesBeforeTrailingComments: 2
WhitespaceSensitiveMacros: ['G_DEFINE_QUARK']

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/meson.build.user
/subprojects/*
!/subprojects/*.wrap
!/subprojects/packagefiles

5
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "wuffs-mirror-release-c"]
path = wuffs-mirror-release-c
path = submodules/wuffs-mirror-release-c
url = https://github.com/google/wuffs-mirror-release-c
[submodule "liberty"]
path = submodules/liberty
url = https://git.janouch.name/p/liberty.git

View File

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

View File

@@ -1,66 +1,104 @@
fastiv
======
fiv
===
'fastiv' is a fast image viewer, supporting BMP, PNG, GIF, JPEG, and optionally
RAW pictures.
'fiv' is a slightly unconventional, general-purpose image browser and viewer
for Linux and Windows (macOS still has major issues).
It is meant to be a viable replacement for Eye of GNOME, which is slow, likes
to break on huge pictures, and its underlying gdk-pixbuf can only be made to use
the broken libopenraw
https://mail.gnome.org/archives/eog-list/2016-January/msg00004.html[as of now].
image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
Further development
-------------------
Urgent blockers for the first stable version:
Features
--------
- Uses a compact thumbnail view, helping you browse collections comfortably.
- Supports BMP, (A)PNG, GIF, TGA, JPEG, WebP directly, plus optionally raw
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.
- 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.
- directory browsing
- implement zoom and scrolling
Explicit non-goals
------------------
- Editing--that's what _editors_ are for, be it GIMP or Rawtherapee;
nothing beyond the most basic of adjustments is desired.
- Following the latest GNOME HIG to the letter--header bars are deliberately
avoided, for their general user hostility.
- Memory efficiency is secondary to both performance and development effort.
High priority:
- some level of asynchronous loading and preloading,
which becomes a difficult problem with network mounts,
confusingly acting as fast devices
- write a replacement for GNOME's Nautilus in grid mode:
read-only, with focus on staggered previews and minimising wasted space
Low priority:
- display 16-bit pictures smoothly, using the 30-bit depth under X.org
- make RAW as fast as it can possibly be
- load everything that resembles a picture, potentially even play video
- port to something less hostile than the current GNOME stack, such as SDL,
although it may involve a lot of reimplemented code,
or result in reduced functionality
Non-goals:
- fancy UI, focus solely on speed of use
- memory efficiency, though preloading can cause some pressure
Aspirations
-----------
Show colours as accurately as hardware allows. Open everything. Be fast.
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 dependencies: Meson, pkg-config +
Runtime dependencies: gtk+-3.0, libpng > 1.5.4, libturbojpeg, LibRaw (optional),
shared-mime-info
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, 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) +
Runtime dependencies for reverse image search:
xdg-utils, cURL, jq
$ git clone --recursive https://git.janouch.name/p/fastiv.git
$ meson builddir
$ git clone --recursive https://git.janouch.name/p/fiv.git
$ cd fiv
$ meson setup builddir
$ cd builddir
$ meson compile
$ meson devenv fiv
To install the application, you can do:
The lossless JPEG cropper and reverse image search are intended to be invoked
from a file manager context menu.
# meson install
For proper integration, you will need to install the application. On Debian,
you can get a quick and dirty installation package for testing purposes using:
$ meson compile deb
# dpkg -i fiv-*.deb
Windows
~~~~~~~
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
dependencies listed at the beginning of 'msys2-configure.sh',
plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
Beware that the build will take up about a gigabyte of disk space.
$ sh -e msys2-configure.sh builddir
$ meson compile package -C builddir
If everything succeeds, you will find a portable build of the application
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
-------------
For information concerning usage, refer to link:docs/fiv.html[the user guide],
which can be invoked from within the program by pressing F1.
Contributing and Support
------------------------
Use https://git.janouch.name/p/fastiv to report any bugs, request features,
Use https://git.janouch.name/p/fiv to report any bugs, request features,
or submit pull requests. `git send-email` is tolerated. If you want to discuss
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.

85
docs/fiv.adoc Normal file
View File

@@ -0,0 +1,85 @@
fiv(1)
======
:doctype: manpage
:manmanual: fiv Manual
:mansource: fiv {release-version}
Name
----
fiv - Image browser and viewer
Synopsis
--------
*fiv* [_OPTION_]... [_PATH_ | _URI_]...
Description
-----------
*fiv* is a general-purpose image browser and viewer: pass it a directory path
or URI to open it for browsing, or pass an image to open it for viewing.
In case that multiple arguments are passed, they'll be opened as a virtual
directory containing all of them.
For more information concerning usage, press *F1* in the application to open
the _User Guide_.
// TODO(p): Try to merge the two, though this one focuses on command line usage.
Options
-------
*--browse*::
When an image is passed, start in browsing mode, and preselect that
image in its containing directory. This is used by *fiv*'s inode/directory
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+.
*--invalidate-cache*::
Invalidate the wide thumbnail cache, removing thumbnails for files that can
no longer be found.
*--list-supported-media-types*::
Output supported media types and exit. This is used by a script to update
the list of MIME types within *fiv*'s desktop file when the list
of GdkPixbuf loaders changes.
*-V*, *--version*::
Output version information and exit.
Internal options
~~~~~~~~~~~~~~~~
*--extract-thumbnail*::
Present any embedded thumbnail of the first argument on the standard output
in an application-specific bitmap format. When both *--thumbnail*
and *--extract-thumbnail* are passed, this option takes precedence,
exiting early if successful. This is used to enhance responsivity
of thumbnail procurement.
*--thumbnail*=_SIZE_::
Generate wide thumbnails for the first argument, in all sizes not exceeding
_SIZE_, and present the largest of them on the standard output
in an application-specific bitmap format. Available sizes follow directory
names in the _Thumbnail Managing Standard_.
*--thumbnail-for-search*=_SIZE_::
Transform the first argument to a widely supported image file format,
and present it on the standard output. The image will be downscaled as
necessary so as to not exceed _SIZE_ (see *--thumbnail*).
Reporting bugs
--------------
Use https://git.janouch.name/p/fiv to report bugs, request features,
or submit pull requests.
See also
--------
_Desktop Entry Specification_,
https://specifications.freedesktop.org/desktop-entry-spec/latest/[].
_Thumbnail Managing Standard_,
https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html[].

133
docs/fiv.html Normal file
View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>fiv: User Guide</title>
<link rel="stylesheet" href="stylesheet.css">
<style>
q:lang(en):before { content: ""; }
q:lang(en):after { content: ""; }
</style>
</head>
<body>
<h1>fiv: User Guide</h1>
<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 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">
<h2>Introduction</h2>
<p><i>fiv</i> is a general-purpose image browser and viewer. This document will
guide you through the application and help to familiarize you with it.
<h2>Controls</h2>
<p><i>fiv</i> is designed with computer mice having dedicated forwards/backwards
and page up/down buttons in mind, such as SteelSeries Sensei series. Ozone Neon
series may also be mapped this way. Your experience may be degraded with other
kinds of devices.
<p>Controls should generally be accessible through the keyboard. Pressing
<kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>?</kbd> will give you a convenient overview
of all shortcuts. In addition to these, remember that you may often use
<kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>Tab</kbd> and <kbd>F6</kbd> to navigate to
different groups of widgets.
<h2>Browser</h2>
<p><i>fiv</i> normally starts in a file browser view. On the left side of the
window, you'll find your GTK+ bookmarks, mounted locations as recognized by
GVfs, an item for entering arbitrary filesystem paths or URIs, and finally
breadcrumbs leading to the currently opened directory, as well as descendants
of it. At the top, there is a toolbar with view controls.
<p>You can open items in a new window either by middle clicking on them, or with
the left mouse button while holding the <kbd>Ctrl</kbd> key.
Right clicking the directory view offers a context menu for opening files,
or even the directory itself, in a different application.
<h2>Viewer</h2>
<p>The image viewer may be both entered (so long as you have a file selected)
and exited using the <kbd>Enter</kbd> key. This way you may easily switch
between the two modes. When using the mouse, the forwards and backwards buttons
will fulfill the same function.
<p>Double clicking the image switches full-screen view, and the mouse wheel
adjusts the zoom level.
<p>Files are iterated in the same order, and using the same filtering as in
the browser.
<h2>File formats</h2>
<p>The list of all supported file formats may be obtained by running:
<pre>
fiv --list-supported-media-types
</pre>
<p>Unless it has been turned off in your installation, you may extend it through
gdk-pixbuf modules.
<h2>Thumbnails</h2>
<p><i>fiv</i> uses a custom means of storing thumbnails, and doesn't currently
invalidate this cache automatically. Should you find out that your
<i>~/.cache/thumbnails</i> directory is taking up too much space, run:
<pre>
fiv --invalidate-cache
</pre>
<p>to trim it down. Alternatively, if you want to get rid of <i>all</i>
thumbnails, even for existing images:
<pre>
rm -rf ~/.cache/thumbnails/wide-*
</pre>
<h2>Configuration</h2>
<p>To adjust the few configuration options of <i>fiv</i>,
press <kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>,</kbd> to open <i>Preferences</i>.
<p>To make your changes take effect, restart <i>fiv</i>.
<h3>Theming</h3>
<p>The standard means to adjust the looks of the program is through GTK+ 3 CSS.
As an example, to tightly pack browser items, put the following in your
<i>~/.config/gtk-3.0/gtk.css</i>:
<pre>
fiv-browser { -FivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; }
</pre>
<p>Similarly, you can adjust some of the key bindings, as per the command table
in the <i>fiv-view.h</i> source file:
<pre>
@binding-set ViewBindings { bind 'p' { 'command' (print) }; }
fiv-view { -gtk-key-bindings: ViewBindings; }
</pre>
<p>Should you want to experiment, you will find the GTK+ inspector very helpful.
<script>
var toc = '', all = document.querySelectorAll('h2')
for (var i = 0; i < all.length; i++) {
var name = all[i].innerHTML.replace(/[^0-9a-z]/ig, '-')
toc += '<li><p><a href="#' + name + '">' + all[i].innerHTML + '</a></li>'
all[i].id = name
all[i].innerHTML = (i + 1) + '. ' + all[i].innerHTML
}
all[0].insertAdjacentHTML('beforebegin',
'<h2>Table of Contents</h2><ol>' + toc + '</ol>')
</script>

BIN
docs/fiv.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

9
docs/stylesheet.css Normal file
View File

@@ -0,0 +1,9 @@
body { max-width: 50em; margin: 0 auto 4em auto; padding: 0 2em;
font-family: sans-serif; } h1, h2, h3 { font-weight: normal; }
h1 { font-size: 2.5em; } h2 { font-size: 2em; } h3 { font-size: 1.33em; }
h2 { padding-top: .67em; border-top: 1px solid silver; }
p { line-height: 1.5; } .figure { text-align: center; } img { max-width: 100%; }
q { font-style: normal; } .details { border-bottom: 1px solid silver; }
.details br { display: none; } .details br + span:before { content: " — "; }
pre { padding: 0 1em; } kbd { border: solid #ccc; border-radius: .25em;
border-width: 1px 2px 2px 1px; padding: 0 .25em; font-family: inherit; }

View File

@@ -0,0 +1,126 @@
Wide Thumbnail Managing Standard
================================
Přemysl Eric Janouch <p@janouch.name>
v0.1, 2021-12-29: Preliminary draft
:description: Wide-format thumbnail managment specification
Introduction
------------
This document is a follow-up to the
https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html[Thumbnail Managing Standard],
in particular version 0.9.0. It extends the specification to cover wide-format
file thumbnails, providing a well-defined mechanism for sharing them amongst
different programs. It also addresses new needs that have arisen with
high-density, wide-gamut monitors.
Please contact the author of this document if you intend to use it.
Rationale
~~~~~~~~~
Photos tend to be in a 4:3 format. Yet, nearly all file browsers at the time
of this document's conception (end of year 2021) show previews in a rectangular
grid. This wastes a lot of display real estate with padding, and looks
subjectively awkward. The Web at large has long been moving off of this
concept, instead preferring freely flowing items of fixed height--notably on
such sites as DeviantArt and Google/Duck image search. The general Unix desktop
keeps lagging behind.
Scaling sub-nominal thumbnail sizes up, or larger sizes down is not a practical
solution: the former gives blurry images, while the latter may waste
a significant amount of disk space. Both require reprocessing. Seeing as these
issues have only become worse with the higher resolutions added to the 0.9.0
revision of the preceding standard, a new one is necessary.
Storage
-------
The base directory for thumbnails is the same as in the original specification.
The list of subdirectories is similar, but a `wide-` prefix is added,
turning `large` into `wide-large`, and so on. The `fail` directory does not
constitute an exception to this rule, and is also duplicated, if necessary.
The dimensions differ like so: the original _height_ for the respective sizes is
kept, but a factor of 2 is applied to the _width_:
- _$XDG_CACHE_HOME/thumbnails/wide-normal_ contains previews proportionally
scaled down to 256x128 pixels,
- _$XDG_CACHE_HOME/thumbnails/wide-large_ contains 512x256 pixel previews,
- _$XDG_CACHE_HOME/thumbnails/wide-x-large_ contains 1024x512 pixel previews,
- _$XDG_CACHE_HOME/thumbnails/wide-xx-large_ contains 2048x1024 pixel previews.
It is unspecified whether non-square pixels are scaled down accordingly, but it
is recommended to do so.
File format
~~~~~~~~~~~
To account for very large thumbnail sizes, this specification has chosen the
WebP codec, in its _extended file format_. Thumbnail files still derive their
name from the MD5 hash of input URIs, as in the original standard, because
this widespread algorithm shows surprisingly good results for this use case.
The filename, however, deviates in that it receives the appropriate _.webp_ file
extension.
Both lossy and lossless encodings may be used. Animations are assumed to be
representative samples, and their timing needn't be respected. No metadata
chunks are allowed other than _THUM_, which is described below. Any Exif
orientation changes need to be "baked-in" to the image.
Metadata
~~~~~~~~
Because WebP doesn't directly provide any means of storing simple key-value
pairs, thumbnail attributes are stored in a custom chunk named "THUM".
It consists of a stream of NUL-terminated pairs, using the UTF-8 encoding.
The last NUL byte may not be omitted. The behavior of repeated keys is
undefined.
All keys from the original specification are adopted, including the extension
mechanism, plus these additions:
.Additional fields
[cols="1,2"]
|===
| Key | Description
| `Thumb::ColorSpace` | The thumbnail's color space, if it is known.
|===
The color space field may only be included if the producing program has applied
color management to the input image, e.g., using any embedded ICC profiles,
so that the color space is now known and normalized. No rendering intent is
hereby suggested. It is permitted to assume sRGB for input images with
unspecified color spaces. The full list of allowed values is:
.Color spaces
[cols="1,2"]
|===
| Value | Description
| `sRGB` | IEC 61966-2-1
| `Display P3` | sRGB with DCI-P3 primaries, as used by Apple.
|===
Interactions
------------
Programs may fall back to picking up and rescaling a square-sized thumbnail if
they fail to find a wide one, preferrably one size above what they are looking
for. The wide-format thumbnail should then be automatically regenerated.
It should also be regenerated if the program supports color management, but it
has found a thumbnail without a color space field.
A _normal_-sized old-specification thumbnail may be produced alongside any
wide ones, but it is strongly suggested to avoid duplicating the larger sizes.
References
----------
- https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html[Thumbnail Managing Standard];
- https://developers.google.com/speed/webp/docs/riff_container[WebP Container Specification];
- https://github.com/rurban/smhasher[smhasher -- Hash function quality and speed tests]
evaluates MD5 as an excellent non-cryptographic hash function;
- https://datatracker.ietf.org/doc/html/rfc3629[RFC 3629 -- UTF-8, a transformation format of ISO 10646];
- IEC 61966-2-1:1999 -- sRGB is "only" available commercially, but reduces to
https://www.color.org/chardata/rgb/srgb.xalter[these characteristics];
- https://developer.apple.com/documentation/coregraphics/cgcolorspace/1408916-displayp3[Apple's brief Display P3 definition]
and the corresponding
https://www.color.org/chardata/rgb/DCIP3.xalter[DCI-P3 characteristics].
Change history
--------------
v0.1, 2021-12-29, Přemysl Eric Janouch::
Preliminary draft.

View File

@@ -1,233 +0,0 @@
//
// fastiv-browser.c: fast image viewer - filesystem browser widget
//
// Copyright (c) 2021, 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 <math.h>
#include "fastiv-browser.h"
#include "fastiv-view.h"
typedef struct entry Entry;
struct entry {
char *filename;
cairo_surface_t *thumbnail;
};
static void
entry_free(Entry *self)
{
g_free(self->filename);
if (self->thumbnail)
cairo_surface_destroy(self->thumbnail);
}
// --- Boilerplate -------------------------------------------------------------
struct _FastivBrowser {
GtkWidget parent_instance;
// TODO(p): We probably want to pre-arrange everything into rows.
// - All rows are the same height.
GArray *entries;
int selected;
};
// TODO(p): For proper navigation, we need to implement GtkScrollable.
G_DEFINE_TYPE_EXTENDED(FastivBrowser, fastiv_browser, GTK_TYPE_WIDGET, 0,
/* G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE,
fastiv_browser_scrollable_init) */)
enum {
ITEM_ACTIVATED,
LAST_SIGNAL,
};
// Globals are, sadly, the canonical way of storing signal numbers.
static guint browser_signals[LAST_SIGNAL];
static void
fastiv_browser_finalize(GObject *gobject)
{
G_GNUC_UNUSED FastivBrowser *self = FASTIV_BROWSER(gobject);
G_OBJECT_CLASS(fastiv_browser_parent_class)->finalize(gobject);
}
static GtkSizeRequestMode
fastiv_browser_get_request_mode(G_GNUC_UNUSED GtkWidget *widget)
{
return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}
static void
fastiv_browser_get_preferred_width(GtkWidget *widget,
gint *minimum, gint *natural)
{
G_GNUC_UNUSED FastivBrowser *self = FASTIV_BROWSER(widget);
// TODO(p): Set it to the width of the widget with one wide item within.
*minimum = *natural = 0;
}
static void
fastiv_browser_get_preferred_height_for_width(GtkWidget *widget,
G_GNUC_UNUSED gint width, gint *minimum, gint *natural)
{
G_GNUC_UNUSED FastivBrowser *self = FASTIV_BROWSER(widget);
// TODO(p): Re-layout, figure it out.
*minimum = *natural = 0;
}
static void
fastiv_browser_realize(GtkWidget *widget)
{
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
GdkWindowAttr attributes = {
.window_type = GDK_WINDOW_CHILD,
.x = allocation.x,
.y = allocation.y,
.width = allocation.width,
.height = allocation.height,
// Input-only would presumably also work (as in GtkPathBar, e.g.),
// but it merely seems to involve more work.
.wclass = GDK_INPUT_OUTPUT,
.visual = gtk_widget_get_visual(widget),
.event_mask = gtk_widget_get_events(widget)
| GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK,
};
// We need this window to receive input events at all.
// TODO(p): See if input events bubble up to parents.
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
gtk_widget_register_window(widget, window);
gtk_widget_set_window(widget, window);
gtk_widget_set_realized(widget, TRUE);
}
static gboolean
fastiv_browser_draw(GtkWidget *widget, cairo_t *cr)
{
G_GNUC_UNUSED FastivBrowser *self = FASTIV_BROWSER(widget);
if (!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
return TRUE;
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
gtk_render_background(gtk_widget_get_style_context(widget), cr,
0, 0, allocation.width, allocation.height);
const double row_height = 256;
gint occupied_width = 0, y = 0;
for (guint i = 0; i < self->entries->len; i++) {
const Entry *entry = &g_array_index(self->entries, Entry, i);
if (!entry->thumbnail)
continue;
int width = cairo_image_surface_get_width(entry->thumbnail);
int height = cairo_image_surface_get_height(entry->thumbnail);
double scale = row_height / height;
if (width * scale > 2 * row_height)
scale = 2 * row_height / width;
int projected_width = round(scale * width);
int projected_height = round(scale * height);
if (occupied_width != 0
&& occupied_width + projected_width > allocation.width) {
occupied_width = 0;
y += row_height;
}
cairo_save(cr);
cairo_translate(cr, occupied_width, y + row_height - projected_height);
cairo_scale(cr, scale, scale);
cairo_set_source_surface(cr, entry->thumbnail, 0, 0);
cairo_paint(cr);
cairo_restore(cr);
occupied_width += projected_width;
}
return TRUE;
}
static void
fastiv_browser_class_init(FastivBrowserClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fastiv_browser_finalize;
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
widget_class->get_request_mode = fastiv_browser_get_request_mode;
widget_class->get_preferred_width =
fastiv_browser_get_preferred_width;
widget_class->get_preferred_height_for_width =
fastiv_browser_get_preferred_height_for_width;
widget_class->realize = fastiv_browser_realize;
widget_class->draw = fastiv_browser_draw;
// TODO(p): Connect to this and emit it.
browser_signals[ITEM_ACTIVATED] =
g_signal_new("item-activated", G_TYPE_FROM_CLASS(klass),
0, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
// TODO(p): Later override "screen_changed", recreate Pango layouts there,
// if we get to have any, or otherwise reflect DPI changes.
gtk_widget_class_set_css_name(widget_class, "fastiv-browser");
}
static void
fastiv_browser_init(FastivBrowser *self)
{
gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
self->entries = g_array_new(FALSE, TRUE, sizeof(Entry));
g_array_set_clear_func(self->entries, (GDestroyNotify) entry_free);
self->selected = -1;
}
void
fastiv_browser_load(FastivBrowser *self, const char *path)
{
g_array_set_size(self->entries, 0);
// TODO(p): Use opendir(), in order to get file type directly.
GDir *dir = g_dir_open(path, 0, NULL);
if (!dir)
return;
const char *filename;
while ((filename = g_dir_read_name(dir))) {
if (!strcmp(filename, ".")
|| !strcmp(filename, ".."))
continue;
gchar *subpath = g_build_filename(path, filename, NULL);
g_array_append_val(self->entries, ((Entry) {
.thumbnail = fastiv_io_lookup_thumbnail(subpath),
.filename = subpath,
}));
}
g_dir_close(dir);
// TODO(p): Sort the entries.
gtk_widget_queue_draw(GTK_WIDGET(self));
}

View File

@@ -1,620 +0,0 @@
//
// fastiv-io.c: image loaders
//
// Copyright (c) 2021, 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 <cairo.h>
#include <turbojpeg.h>
#ifdef HAVE_LIBRAW
#include <libraw.h>
#endif // HAVE_LIBRAW
#define WUFFS_IMPLEMENTATION
#define WUFFS_CONFIG__MODULES
#define WUFFS_CONFIG__MODULE__ADLER32
#define WUFFS_CONFIG__MODULE__BASE
#define WUFFS_CONFIG__MODULE__BMP
#define WUFFS_CONFIG__MODULE__CRC32
#define WUFFS_CONFIG__MODULE__DEFLATE
#define WUFFS_CONFIG__MODULE__GIF
#define WUFFS_CONFIG__MODULE__LZW
#define WUFFS_CONFIG__MODULE__PNG
#define WUFFS_CONFIG__MODULE__ZLIB
#include "wuffs-mirror-release-c/release/c/wuffs-v0.3.c"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define FASTIV_IO_ERROR fastiv_io_error_quark()
G_DEFINE_QUARK(fastiv-io-error-quark, fastiv_io_error)
enum FastivIoError {
FASTIV_IO_ERROR_OPEN,
};
static void
set_error(GError **error, const char *message)
{
g_set_error_literal(error,
FASTIV_IO_ERROR, FASTIV_IO_ERROR_OPEN, message);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// https://github.com/google/wuffs/blob/main/example/gifplayer/gifplayer.c
// is pure C, and a good reference. I can't use the auxiliary libraries,
// since they depend on C++, which is undesirable.
static cairo_surface_t *
open_wuffs(wuffs_base__image_decoder *dec,
wuffs_base__io_buffer src, GError **error)
{
wuffs_base__image_config cfg;
wuffs_base__status status =
wuffs_base__image_decoder__decode_image_config(dec, &cfg, &src);
if (!wuffs_base__status__is_ok(&status)) {
set_error(error, wuffs_base__status__message(&status));
return NULL;
}
if (!wuffs_base__image_config__is_valid(&cfg)) {
set_error(error, "invalid Wuffs image configuration");
return NULL;
}
// We need to check because of the Cairo API.
uint32_t width = wuffs_base__pixel_config__width(&cfg.pixcfg);
uint32_t height = wuffs_base__pixel_config__height(&cfg.pixcfg);
if (width > INT_MAX || height > INT_MAX) {
set_error(error, "image dimensions overflow");
return NULL;
}
// CAIRO_FORMAT_ARGB32: "The 32-bit quantities are stored native-endian.
// Pre-multiplied alpha is used."
// CAIRO_FORMAT_RGB{24,30}: analogous, not going to use these so far.
//
// Wuffs: /doc/note/pixel-formats.md specifies it as "memory order", which,
// for our purposes, means big endian. Currently useful formats, as per
// support within wuffs_base__pixel_swizzler__prepare__*():
// - WUFFS_BASE__PIXEL_FORMAT__ARGB_PREMUL: big-endian
// - WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL: little-endian
// - WUFFS_BASE__PIXEL_FORMAT__XRGB: big-endian
// - WUFFS_BASE__PIXEL_FORMAT__BGRX: little-endian
//
// TODO(p): Make Wuffs support RGB30 as a destination format,
// so far we only have WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL_4X16LE
// and in general, 16-bit depth swizzlers are stubbed.
wuffs_base__pixel_config__set(&cfg.pixcfg,
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL,
#else
WUFFS_BASE__PIXEL_FORMAT__ARGB_PREMUL,
#endif
WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height);
wuffs_base__slice_u8 workbuf = {0};
uint64_t workbuf_len_max_incl =
wuffs_base__image_decoder__workbuf_len(dec).max_incl;
if (workbuf_len_max_incl) {
workbuf = wuffs_base__malloc_slice_u8(malloc, workbuf_len_max_incl);
if (!workbuf.ptr) {
set_error(error, "failed to allocate a work buffer");
return NULL;
}
}
cairo_surface_t *surface =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
cairo_status_t surface_status = cairo_surface_status(surface);
if (surface_status != CAIRO_STATUS_SUCCESS) {
set_error(error, cairo_status_to_string(surface_status));
cairo_surface_destroy(surface);
free(workbuf.ptr);
return NULL;
}
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
// ARGB/BGR/XRGB/BGRX. This function does not support a stride different
// from the width, maybe Wuffs internals do not either.
wuffs_base__pixel_buffer pb = {0};
status = wuffs_base__pixel_buffer__set_from_slice(&pb, &cfg.pixcfg,
wuffs_base__make_slice_u8(cairo_image_surface_get_data(surface),
cairo_image_surface_get_stride(surface) *
cairo_image_surface_get_height(surface)));
if (!wuffs_base__status__is_ok(&status)) {
set_error(error, wuffs_base__status__message(&status));
cairo_surface_destroy(surface);
free(workbuf.ptr);
return NULL;
}
#if 0 // We're not using this right now.
wuffs_base__frame_config fc = {0};
status = wuffs_png__decoder__decode_frame_config(&dec, &fc, &src);
if (!wuffs_base__status__is_ok(&status)) {
set_error(error, wuffs_base__status__message(&status));
cairo_surface_destroy(surface);
free(workbuf.ptr);
return NULL;
}
#endif
// Starting to modify pixel data directly. Probably an unnecessary call.
cairo_surface_flush(surface);
status = wuffs_base__image_decoder__decode_frame(dec, &pb, &src,
WUFFS_BASE__PIXEL_BLEND__SRC, workbuf, NULL);
if (!wuffs_base__status__is_ok(&status)) {
set_error(error, wuffs_base__status__message(&status));
cairo_surface_destroy(surface);
free(workbuf.ptr);
return NULL;
}
// Pixel data has been written, need to let Cairo know.
cairo_surface_mark_dirty(surface);
free(workbuf.ptr);
return surface;
}
static cairo_surface_t *
open_wuffs_using(wuffs_base__image_decoder *(*allocate)(),
const gchar *data, gsize len, GError **error)
{
wuffs_base__image_decoder *dec = allocate();
if (!dec) {
set_error(error, "memory allocation failed or internal error");
return NULL;
}
cairo_surface_t *surface = open_wuffs(dec,
wuffs_base__ptr_u8__reader((uint8_t *) data, len, TRUE), error);
free(dec);
return surface;
}
static void
trivial_cmyk_to_bgra(unsigned char *p, int len)
{
// Inspired by gdk-pixbuf's io-jpeg.c:
//
// 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];
p[0] = k * y / 255;
p[1] = k * m / 255;
p[2] = k * c / 255;
p[3] = 255;
p += 4;
}
}
static cairo_surface_t *
open_libjpeg_turbo(const gchar *data, gsize len, GError **error)
{
tjhandle dec = tjInitDecompress();
if (!dec) {
set_error(error, tjGetErrorStr2(dec));
return NULL;
}
int width = 0, height = 0, subsampling = TJSAMP_444, colorspace = TJCS_RGB;
if (tjDecompressHeader3(dec, (const unsigned char *) data, len,
&width, &height, &subsampling, &colorspace)) {
set_error(error, tjGetErrorStr2(dec));
tjDestroy(dec);
return NULL;
}
int pixel_format = (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK)
? TJPF_CMYK
: (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRA : TJPF_ARGB);
cairo_surface_t *surface =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
cairo_status_t surface_status = cairo_surface_status(surface);
if (surface_status != CAIRO_STATUS_SUCCESS) {
set_error(error, cairo_status_to_string(surface_status));
cairo_surface_destroy(surface);
tjDestroy(dec);
return NULL;
}
// Starting to modify pixel data directly. Probably an unnecessary call.
cairo_surface_flush(surface);
int stride = cairo_image_surface_get_stride(surface);
if (tjDecompress2(dec, (const unsigned char *) data, len,
cairo_image_surface_get_data(surface), width, stride,
height, pixel_format, TJFLAG_ACCURATEDCT)) {
set_error(error, tjGetErrorStr2(dec));
cairo_surface_destroy(surface);
tjDestroy(dec);
return NULL;
}
// TODO(p): Support big-endian machines, too, those need ARGB ordering.
if (pixel_format == TJPF_CMYK) {
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
// ARGB/BGR/XRGB/BGRX.
trivial_cmyk_to_bgra(cairo_image_surface_get_data(surface),
width * height);
}
// Pixel data has been written, need to let Cairo know.
cairo_surface_mark_dirty(surface);
tjDestroy(dec);
return surface;
}
#ifdef HAVE_LIBRAW // ---------------------------------------------------------
static cairo_surface_t *
open_libraw(const gchar *data, gsize len, GError **error)
{
// https://github.com/LibRaw/LibRaw/issues/418
libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_MEMERR_CALLBACK
| LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
if (!iprc) {
set_error(error, "failed to obtain a LibRaw handle");
return NULL;
}
#if 0
// TODO(p): Consider setting this--the image is still likely to be
// rendered suboptimally, so why not make it faster.
iprc->params.half_size = 1;
#endif
// TODO(p): Check if we need to set anything for autorotation (sizes.flip).
iprc->params.use_camera_wb = 1;
iprc->params.output_color = 1; // sRGB, TODO(p): Is this used?
iprc->params.output_bps = 8; // This should be the default value.
int err = 0;
if ((err = libraw_open_buffer(iprc, (void *) data, len))) {
set_error(error, libraw_strerror(err));
libraw_close(iprc);
return NULL;
}
// TODO(p): Do we need to check iprc->idata.raw_count? Maybe for TIFFs?
if ((err = libraw_unpack(iprc))) {
set_error(error, libraw_strerror(err));
libraw_close(iprc);
return NULL;
}
#if 0
// TODO(p): I'm not sure when this is necessary or useful yet.
if ((err = libraw_adjust_sizes_info_only(iprc))) {
set_error(error, libraw_strerror(err));
libraw_close(iprc);
return NULL;
}
#endif
// TODO(p): Documentation says I should look at the code and do it myself.
if ((err = libraw_dcraw_process(iprc))) {
set_error(error, libraw_strerror(err));
libraw_close(iprc);
return NULL;
}
// FIXME: This is shittily written to iterate over the range of
// idata.colors, and will be naturally slow.
libraw_processed_image_t *image = libraw_dcraw_make_mem_image(iprc, &err);
if (!image) {
set_error(error, libraw_strerror(err));
libraw_close(iprc);
return NULL;
}
// This should have been transformed, and kept, respectively.
if (image->colors != 3 || image->bits != 8) {
set_error(error, "unexpected number of colours, or bit depth");
libraw_dcraw_clear_mem(image);
libraw_close(iprc);
return NULL;
}
int width = image->width, height = image->height;
cairo_surface_t *surface =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
cairo_status_t surface_status = cairo_surface_status(surface);
if (surface_status != CAIRO_STATUS_SUCCESS) {
set_error(error, cairo_status_to_string(surface_status));
cairo_surface_destroy(surface);
libraw_dcraw_clear_mem(image);
libraw_close(iprc);
return NULL;
}
// Starting to modify pixel data directly. Probably an unnecessary call.
cairo_surface_flush(surface);
uint32_t *pixels = (uint32_t *) cairo_image_surface_get_data(surface);
unsigned char *p = image->data;
for (ushort y = 0; y < image->height; y++) {
for (ushort x = 0; x < image->width; x++) {
*pixels++ = 0xff000000 | (uint32_t) p[0] << 16
| (uint32_t) p[1] << 8 | (uint32_t) p[2];
p += 3;
}
}
// Pixel data has been written, need to let Cairo know.
cairo_surface_mark_dirty(surface);
libraw_dcraw_clear_mem(image);
libraw_close(iprc);
return surface;
}
#endif // HAVE_LIBRAW ---------------------------------------------------------
cairo_surface_t *
fastiv_io_open(const gchar *path, GError **error)
{
// TODO(p): Don't always load everything into memory, test type first,
// for which we only need the first 16 bytes right now.
// Though LibRaw poses an issue--we may want to try to map RAW formats
// to FourCC values--many of them are compliant TIFF files.
// We might want to employ a more generic way of magic identification,
// and with some luck, it could even be integrated into Wuffs.
gchar *data = NULL;
gsize len = 0;
if (!g_file_get_contents(path, &data, &len, error))
return NULL;
wuffs_base__slice_u8 prefix =
wuffs_base__make_slice_u8((uint8_t *) data, len);
cairo_surface_t *surface = NULL;
switch (wuffs_base__magic_number_guess_fourcc(prefix)) {
case WUFFS_BASE__FOURCC__BMP:
// Note that BMP can redirect into another format,
// which is so far unsupported here.
surface = open_wuffs_using(
wuffs_bmp__decoder__alloc_as__wuffs_base__image_decoder,
data, len, error);
break;
case WUFFS_BASE__FOURCC__GIF:
surface = open_wuffs_using(
wuffs_gif__decoder__alloc_as__wuffs_base__image_decoder,
data, len, error);
break;
case WUFFS_BASE__FOURCC__PNG:
surface = open_wuffs_using(
wuffs_png__decoder__alloc_as__wuffs_base__image_decoder,
data, len, error);
break;
case WUFFS_BASE__FOURCC__JPEG:
surface = open_libjpeg_turbo(data, len, error);
break;
default:
#ifdef HAVE_LIBRAW // ---------------------------------------------------------
if ((surface = open_libraw(data, len, error)))
break;
// TODO(p): We should try to pass actual processing errors through,
// notably only continue with LIBRAW_FILE_UNSUPPORTED.
g_clear_error(error);
#endif // HAVE_LIBRAW ---------------------------------------------------------
// TODO(p): Integrate gdk-pixbuf as a fallback (optional dependency).
set_error(error, "unsupported file type");
}
free(data);
return surface;
}
// --- Thumbnails --------------------------------------------------------------
// NOTE: "It is important to note that when an image with an alpha channel is
// scaled, linear encoded, pre-multiplied component values must be used!"
//
// We can use the pixman library to scale, PIXMAN_a8r8g8b8_sRGB.
#include <png.h>
#include <glib/gstdio.h>
// TODO(p): Reorganize the sources.
gchar *get_xdg_home_dir(const char *var, const char *default_);
static void
redirect_png_error(png_structp pngp, const char *error)
{
set_error(png_get_error_ptr(pngp), error);
png_longjmp(pngp, 1);
}
static void
discard_png_warning(png_structp pngp, const char *warning)
{
(void) pngp;
(void) warning;
}
static int
check_png_thumbnail(png_structp pngp, png_infop infop, const gchar *target,
time_t mtime)
{
// May contain Thumb::Image::Width Thumb::Image::Height,
// but those aren't interesting currently (would be for fast previews).
int texts_len = 0;
png_textp texts = NULL;
png_get_text(pngp, infop, &texts, &texts_len);
gboolean need_uri = TRUE, need_mtime = TRUE;
for (int i = 0; i < texts_len; i++) {
png_textp text = texts + i;
if (!strcmp(text->key, "Thumb::URI")) {
need_uri = FALSE;
if (strcmp(target, text->text))
return FALSE;
}
if (!strcmp(text->key, "Thumb::MTime")) {
need_mtime = FALSE;
if (atol(text->text) != mtime)
return FALSE;
}
}
return need_uri || need_mtime ? -1 : TRUE;
}
// TODO(p): Support spng as well (it can't premultiply alpha by itself,
// but at least it won't gamma-adjust it for us).
static cairo_surface_t *
read_png_thumbnail(const gchar *path, const gchar *uri, time_t mtime,
GError **error)
{
FILE *fp;
if (!(fp = fopen(path, "rb"))) {
set_error(error, g_strerror(errno));
return NULL;
}
cairo_surface_t *volatile surface = NULL;
png_structp pngp = png_create_read_struct(PNG_LIBPNG_VER_STRING,
error, redirect_png_error, discard_png_warning);
png_infop infop = png_create_info_struct(pngp);
if (!infop) {
set_error(error, g_strerror(errno));
goto fail_preread;
}
volatile png_bytepp row_pointers = NULL;
if (setjmp(png_jmpbuf(pngp))) {
if (surface) {
cairo_surface_destroy(surface);
surface = NULL;
}
goto fail;
}
png_init_io(pngp, fp);
// XXX: libpng will premultiply with the alpha, but it also gamma-adjust it.
png_set_alpha_mode(pngp, PNG_ALPHA_BROKEN, PNG_DEFAULT_sRGB);
png_read_info(pngp, infop);
if (check_png_thumbnail(pngp, infop, uri, mtime) == FALSE)
png_error(pngp, "mismatch");
// Asking for at least 8-bit channels. This call is a superset of:
// - png_set_palette_to_rgb(),
// - png_set_tRNS_to_alpha(),
// - png_set_expand_gray_1_2_4_to_8().
png_set_expand(pngp);
// Reduce the possibilities further to RGB or RGBA...
png_set_gray_to_rgb(pngp);
// ...and /exactly/ 8-bit channels.
// Alternatively, use png_set_expand_16() above to obtain 16-bit channels.
png_set_scale_16(pngp);
// PNG uses RGBA order, we want either ARGB (BE) or BGRA (LE).
if (G_BYTE_ORDER == G_LITTLE_ENDIAN) {
png_set_bgr(pngp);
png_set_add_alpha(pngp, 0xFFFF, PNG_FILLER_AFTER);
png_set_swap(pngp);
} else {
// This doesn't change a row's `color_type` in png_do_read_filler(),
// and the following transformation thus ignores it.
png_set_add_alpha(pngp, 0xFFFF, PNG_FILLER_BEFORE);
png_set_swap_alpha(pngp);
}
(void) png_set_interlace_handling(pngp);
png_read_update_info(pngp, infop);
png_uint_32 w = png_get_image_width(pngp, infop);
png_uint_32 h = png_get_image_height(pngp, infop);
if (w > INT16_MAX || h > INT16_MAX)
png_error(pngp, "the image is too large");
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
cairo_status_t surface_status = cairo_surface_status(surface);
if (surface_status != CAIRO_STATUS_SUCCESS)
png_error(pngp, cairo_status_to_string(surface_status));
size_t row_bytes = png_get_rowbytes(pngp, infop);
g_assert((size_t) cairo_image_surface_get_stride(surface) == row_bytes);
unsigned char *buffer = cairo_image_surface_get_data(surface);
png_uint_32 height = png_get_image_height(pngp, infop);
if (!(row_pointers = calloc(height, sizeof *row_pointers)))
png_error(pngp, g_strerror(errno));
for (size_t y = 0; y < height; y++)
row_pointers[y] = buffer + y * row_bytes;
cairo_surface_flush(surface);
png_read_image(pngp, row_pointers);
cairo_surface_mark_dirty(surface);
// The specification does not say where the required metadata should be,
// it could very well be broken up into two parts.
png_read_end(pngp, infop);
if (check_png_thumbnail(pngp, infop, uri, mtime) != TRUE)
png_error(pngp, "mismatch or not a thumbnail");
fail:
free(row_pointers);
fail_preread:
png_destroy_read_struct(&pngp, &infop, NULL);
fclose(fp);
return surface;
}
cairo_surface_t *
fastiv_io_lookup_thumbnail(const gchar *target)
{
GStatBuf st;
if (g_stat(target, &st))
return NULL;
// TODO(p): Consider making the `target` an absolute path, if it isn't.
// Or maybe let it fail, and document the requirement.
gchar *uri = g_filename_to_uri(target, NULL, NULL);
if (!uri)
return NULL;
gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
cairo_surface_t *result = NULL;
const gchar *sizes[] = {"large", "x-large", "xx-large", "normal"};
GError *error = NULL;
for (gsize i = 0; !result && i < G_N_ELEMENTS(sizes); i++) {
gchar *path = g_strdup_printf("%s/thumbnails/%s/%s.png",
cache_dir, sizes[i], sum);
result = read_png_thumbnail(path, uri, st.st_mtim.tv_sec, &error);
if (error) {
g_debug("%s: %s", path, error->message);
g_clear_error(&error);
}
g_free(path);
}
g_free(cache_dir);
g_free(sum);
g_free(uri);
return result;
}

View File

@@ -1,205 +0,0 @@
//
// fastiv-view.c: fast image viewer - view widget
//
// Copyright (c) 2021, 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 <math.h>
#include "fastiv-view.h"
struct _FastivView {
GtkWidget parent_instance;
cairo_surface_t *surface;
// TODO(p): Zoom-to-fit indication.
double scale;
};
G_DEFINE_TYPE(FastivView, fastiv_view, GTK_TYPE_WIDGET)
static int
get_display_width(FastivView *self)
{
if (!self->surface)
return 0;
return ceil(cairo_image_surface_get_width(self->surface) * self->scale);
}
static int
get_display_height(FastivView *self)
{
if (!self->surface)
return 0;
return ceil(cairo_image_surface_get_height(self->surface) * self->scale);
}
static void
fastiv_view_finalize(GObject *gobject)
{
FastivView *self = FASTIV_VIEW(gobject);
cairo_surface_destroy(self->surface);
G_OBJECT_CLASS(fastiv_view_parent_class)->finalize(gobject);
}
static void
fastiv_view_get_preferred_height(GtkWidget *widget,
gint *minimum, gint *natural)
{
*minimum = 0;
*natural = 0;
FastivView *self = FASTIV_VIEW(widget);
*natural = get_display_height(self);
}
static void
fastiv_view_get_preferred_width(GtkWidget *widget,
gint *minimum, gint *natural)
{
*minimum = 0;
*natural = 0;
FastivView *self = FASTIV_VIEW(widget);
*natural = get_display_width(self);
}
static void
fastiv_view_realize(GtkWidget *widget)
{
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
GdkWindowAttr attributes = {
.window_type = GDK_WINDOW_CHILD,
.x = allocation.x,
.y = allocation.y,
.width = allocation.width,
.height = allocation.height,
// Input-only would presumably also work (as in GtkPathBar, e.g.),
// but it merely seems to involve more work.
.wclass = GDK_INPUT_OUTPUT,
// Assuming here that we can't ask for a higher-precision Visual
// than what we get automatically.
.visual = gtk_widget_get_visual(widget),
.event_mask = gtk_widget_get_events(widget) | GDK_SCROLL_MASK,
};
// We need this window to receive input events at all.
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
gtk_widget_register_window(widget, window);
gtk_widget_set_window(widget, window);
gtk_widget_set_realized(widget, TRUE);
}
static gboolean
fastiv_view_draw(GtkWidget *widget, cairo_t *cr)
{
FastivView *self = FASTIV_VIEW(widget);
if (!self->surface
|| !gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
return TRUE;
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
gtk_render_background(gtk_widget_get_style_context(widget), cr,
0, 0, allocation.width, allocation.height);
int w = get_display_width(self);
int h = get_display_height(self);
double x = 0;
double y = 0;
if (w < allocation.width)
x = round((allocation.width - w) / 2.);
if (h < allocation.height)
y = round((allocation.height - h) / 2.);
cairo_scale(cr, self->scale, self->scale);
cairo_set_source_surface(cr, self->surface,
x / self->scale, y / self->scale);
// TODO(p): Prescale it ourselves to an off-screen bitmap, gamma-correctly.
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD);
cairo_paint(cr);
return TRUE;
}
static gboolean
fastiv_view_scroll_event(GtkWidget *widget, GdkEventScroll *event)
{
FastivView *self = FASTIV_VIEW(widget);
if (!self->surface)
return FALSE;
switch (event->direction) {
case GDK_SCROLL_UP:
self->scale *= 1.4;
gtk_widget_queue_resize(widget);
return TRUE;
case GDK_SCROLL_DOWN:
self->scale /= 1.4;
gtk_widget_queue_resize(widget);
return TRUE;
default:
return FALSE;
}
}
static void
fastiv_view_class_init(FastivViewClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fastiv_view_finalize;
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
widget_class->get_preferred_height = fastiv_view_get_preferred_height;
widget_class->get_preferred_width = fastiv_view_get_preferred_width;
widget_class->realize = fastiv_view_realize;
widget_class->draw = fastiv_view_draw;
widget_class->scroll_event = fastiv_view_scroll_event;
// TODO(p): Later override "screen_changed", recreate Pango layouts there,
// if we get to have any, or otherwise reflect DPI changes.
gtk_widget_class_set_css_name(widget_class, "fastiv-view");
}
static void
fastiv_view_init(FastivView *self)
{
self->scale = 1.0;
}
// --- Picture loading ---------------------------------------------------------
// TODO(p): Progressive picture loading, or at least async/cancellable.
gboolean
fastiv_view_open(FastivView *self, const gchar *path, GError **error)
{
cairo_surface_t *surface = fastiv_io_open(path, error);
if (!surface)
return FALSE;
if (self->surface)
cairo_surface_destroy(self->surface);
self->surface = surface;
gtk_widget_queue_resize(GTK_WIDGET(self));
return TRUE;
}

531
fastiv.c
View File

@@ -1,531 +0,0 @@
//
// fastiv.c: fast image viewer
//
// Copyright (c) 2021, 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 <gtk/gtk.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <locale.h>
#include <fnmatch.h>
#include "config.h"
#include "fastiv-view.h"
#include "fastiv-browser.h"
// --- Utilities ---------------------------------------------------------------
static void
exit_fatal(const gchar *format, ...) G_GNUC_PRINTF(1, 2);
static void
exit_fatal(const gchar *format, ...)
{
va_list ap;
va_start(ap, format);
gchar *format_nl = g_strdup_printf("%s\n", format);
vfprintf(stderr, format_nl, ap);
free(format_nl);
va_end(ap);
exit(EXIT_FAILURE);
}
/// Add `element` to the `output` set. `relation` is a map of sets of strings
/// defining is-a relations, and is traversed recursively.
static void
add_applying_transitive_closure(const gchar *element, GHashTable *relation,
GHashTable *output)
{
// Stop condition.
if (!g_hash_table_add(output, g_strdup(element)))
return;
// TODO(p): Iterate over all aliases of `element` in addition to
// any direct match (and rename this no-longer-generic function).
GHashTable *targets = g_hash_table_lookup(relation, element);
if (!targets)
return;
GHashTableIter iter;
g_hash_table_iter_init(&iter, targets);
gpointer key = NULL, value = NULL;
while (g_hash_table_iter_next(&iter, &key, &value))
add_applying_transitive_closure(key, relation, output);
}
// --- XDG ---------------------------------------------------------------------
gchar *
get_xdg_home_dir(const char *var, const char *default_)
{
const char *env = getenv(var);
if (env && *env == '/')
return g_strdup(env);
// The specification doesn't handle a missing HOME variable explicitly.
// Implicitly, assuming Bourne shell semantics, it simply resolves empty.
const char *home = getenv("HOME");
return g_build_filename(home ? home : "", default_, NULL);
}
static gchar **
get_xdg_data_dirs(void)
{
// GStrvBuilder is too new, it would help a little bit.
GPtrArray *output = g_ptr_array_new_with_free_func(g_free);
g_ptr_array_add(output, get_xdg_home_dir("XDG_DATA_HOME", ".local/share"));
const gchar *xdg_data_dirs;
if (!(xdg_data_dirs = getenv("XDG_DATA_DIRS")) || !*xdg_data_dirs)
xdg_data_dirs = "/usr/local/share/:/usr/share/";
gchar **candidates = g_strsplit(xdg_data_dirs, ":", 0);
for (gchar **p = candidates; *p; p++) {
if (**p == '/')
g_ptr_array_add(output, *p);
else
g_free(*p);
}
g_free(candidates);
g_ptr_array_add(output, NULL);
return (gchar **) g_ptr_array_free(output, FALSE);
}
// --- Filtering ---------------------------------------------------------------
// Derived from shared-mime-info-spec 0.21.
// TODO(p): Move to fastiv-io.c, expose the prototype in a header file
// (perhaps finally start a new one for it).
// A subset of shared-mime-info that produces an appropriate list of
// file extensions. Chiefly motivated by the suckiness of RAW images:
// someone else will maintain the list of file extensions for us.
static const char *supported_media_types[] = {
"image/bmp",
"image/gif",
"image/png",
"image/jpeg",
#ifdef HAVE_LIBRAW
"image/x-dcraw",
#endif // HAVE_LIBRAW
};
static void
read_mime_subclasses(const gchar *path, GHashTable *subclass_sets)
{
gchar *data = NULL;
if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */))
return;
// The format of this file is unspecified,
// but in practice it's a list of space-separated media types.
gchar *datasave = NULL;
for (gchar *line = strtok_r(data, "\r\n", &datasave); line;
line = strtok_r(NULL, "\r\n", &datasave)) {
gchar *linesave = NULL,
*subclass = strtok_r(line, " ", &linesave),
*superclass = strtok_r(NULL, " ", &linesave);
// Nothing about comments is specified, we're being nice.
if (!subclass || *subclass == '#' || !superclass)
continue;
GHashTable *set = NULL;
if (!(set = g_hash_table_lookup(subclass_sets, superclass))) {
set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
g_hash_table_insert(subclass_sets, g_strdup(superclass), set);
}
g_hash_table_add(set, g_strdup(subclass));
}
g_free(data);
}
static gboolean
filter_mime_globs(const gchar *path, guint is_globs2, GHashTable *supported_set,
GHashTable *output_set)
{
gchar *data = NULL;
if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */))
return FALSE;
gchar *datasave = NULL;
for (const gchar *line = strtok_r(data, "\r\n", &datasave); line;
line = strtok_r(NULL, "\r\n", &datasave)) {
if (*line == '#')
continue;
// We do not support __NOGLOBS__, nor even parse out the "cs" flag.
// The weight is irrelevant.
gchar **f = g_strsplit(line, ":", 0);
if (g_strv_length(f) >= is_globs2 + 2) {
const gchar *type = f[is_globs2 + 0], *glob = f[is_globs2 + 1];
if (g_hash_table_contains(supported_set, type))
g_hash_table_add(output_set, g_utf8_strdown(glob, -1));
}
g_strfreev(f);
}
g_free(data);
return TRUE;
}
static gchar **
get_supported_globs (void)
{
gchar **data_dirs = get_xdg_data_dirs();
// The mime.cache format is inconvenient to parse,
// we'll do it from the text files manually, and once only.
GHashTable *subclass_sets = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free, (GDestroyNotify) g_hash_table_destroy);
for (gsize i = 0; data_dirs[i]; i++) {
gchar *path =
g_build_filename(data_dirs[i], "mime", "subclasses", NULL);
read_mime_subclasses(path, subclass_sets);
g_free(path);
}
// A hash set of all supported media types, including subclasses,
// but not aliases.
GHashTable *supported =
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
for (gsize i = 0; i < G_N_ELEMENTS(supported_media_types); i++) {
add_applying_transitive_closure(supported_media_types[i],
subclass_sets, supported);
}
g_hash_table_destroy(subclass_sets);
// We do not support the distinction of case-sensitive globs (:cs).
GHashTable *globs =
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
for (gsize i = 0; data_dirs[i]; i++) {
gchar *path2 = g_build_filename(data_dirs[i], "mime", "globs2", NULL);
gchar *path1 = g_build_filename(data_dirs[i], "mime", "globs", NULL);
if (!filter_mime_globs(path2, TRUE, supported, globs))
filter_mime_globs(path1, FALSE, supported, globs);
g_free(path2);
g_free(path1);
}
g_strfreev(data_dirs);
g_hash_table_destroy(supported);
gchar **result = (gchar **) g_hash_table_get_keys_as_array(globs, NULL);
g_hash_table_steal_all(globs);
g_hash_table_destroy(globs);
return result;
}
// --- Main --------------------------------------------------------------------
struct {
gchar **supported_globs;
gchar *directory;
GPtrArray *files;
gint files_index;
gchar *basename;
GtkWidget *window;
GtkWidget *view;
GtkWidget *browser;
GtkWidget *browser_scroller;
} g;
static gboolean
is_supported(const gchar *filename)
{
gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
if (!utf8)
return FALSE;
gchar *lowercased = g_utf8_strdown(utf8, -1);
g_free(utf8);
// XXX: fnmatch() uses the /locale/ encoding, but who cares nowadays.
for (gchar **p = g.supported_globs; *p; p++)
if (!fnmatch(*p, lowercased, 0)) {
g_free(lowercased);
return TRUE;
}
g_free(lowercased);
return FALSE;
}
static void
show_error_dialog(GError *error)
{
GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(g.window),
GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
g_error_free(error);
}
static void
load_directory(const gchar *dirname)
{
free(g.directory);
g.directory = g_strdup(dirname);
g_ptr_array_set_size(g.files, 0);
g.files_index = -1;
fastiv_browser_load(FASTIV_BROWSER(g.browser), dirname);
GError *error = NULL;
GDir *dir = g_dir_open(dirname, 0, &error);
if (dir) {
for (const gchar *name = NULL; (name = g_dir_read_name(dir)); ) {
// This really wants to make you use readdir() directly.
char *absolute = g_canonicalize_filename(name, g.directory);
gboolean is_dir = g_file_test(absolute, G_FILE_TEST_IS_DIR);
g_free(absolute);
if (is_dir || !is_supported(name))
continue;
// XXX: We presume that this basename is from the same directory.
if (!g_strcmp0(g.basename, name))
g.files_index = g.files->len;
g_ptr_array_add(g.files, g_strdup(name));
}
g_dir_close(dir);
} else {
show_error_dialog(error);
}
g_ptr_array_add(g.files, NULL);
// XXX: When something outside the filtered entries is open, the index is
// kept at -1, and browsing doesn't work. How to behave here?
// Should we add it to the pointer array as an exception?
}
static void
open(const gchar *path)
{
g_return_if_fail(g_path_is_absolute(path));
GError *error = NULL;
if (!fastiv_view_open(FASTIV_VIEW(g.view), path, &error)) {
show_error_dialog(error);
return;
}
gtk_window_set_title(GTK_WINDOW(g.window), path);
gchar *basename = g_path_get_basename(path);
g_free(g.basename);
g.basename = basename;
// So that load_directory() itself can be used for reloading.
gchar *dirname = g_path_get_dirname(path);
if (!g.directory || strcmp(dirname, g.directory)) {
load_directory(dirname);
} else {
g.files_index = -1;
for (guint i = 0; i + 1 < g.files->len; i++) {
if (!g_strcmp0(g.basename, g_ptr_array_index(g.files, i)))
g.files_index = i;
}
}
g_free(dirname);
}
static void
on_open(void)
{
GtkWidget *dialog = gtk_file_chooser_dialog_new("Open file",
GTK_WINDOW(g.window), GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT, NULL);
// NOTE: gdk-pixbuf has gtk_file_filter_add_pixbuf_formats().
GtkFileFilter *filter = gtk_file_filter_new();
for (gsize i = 0; i < G_N_ELEMENTS(supported_media_types); i++)
gtk_file_filter_add_mime_type(filter, supported_media_types[i]);
gtk_file_filter_set_name(filter, "Supported images");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
GtkFileFilter *all_files = gtk_file_filter_new();
gtk_file_filter_set_name(all_files, "All files");
gtk_file_filter_add_pattern(all_files, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), all_files);
// The default is local-only, single item. Paths are returned absolute.
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
gchar *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
open(path);
g_free(path);
}
gtk_widget_destroy(dialog);
}
static void
on_previous(void)
{
if (g.files_index >= 0) {
int previous =
(g.files->len - 1 + g.files_index - 1) % (g.files->len - 1);
char *absolute =
g_canonicalize_filename(g_ptr_array_index(g.files, previous),
g.directory);
open(absolute);
g_free(absolute);
}
}
static void
on_next(void)
{
if (g.files_index >= 0) {
int next = (g.files_index + 1) % (g.files->len - 1);
char *absolute =
g_canonicalize_filename(g_ptr_array_index(g.files, next),
g.directory);
open(absolute);
g_free(absolute);
}
}
int
main(int argc, char *argv[])
{
if (!setlocale(LC_CTYPE, ""))
exit_fatal("cannot set locale");
gboolean show_version = FALSE;
gchar **path_args = NULL;
const GOptionEntry options[] = {
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args,
NULL, "[FILE | DIRECTORY]"},
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
&show_version, "output version information and exit", NULL},
{},
};
GError *error = NULL;
if (!gtk_init_with_args(&argc, &argv, " - fast image viewer",
options, NULL, &error))
exit_fatal("%s", error->message);
if (show_version) {
printf(PROJECT_NAME " " PROJECT_VERSION "\n");
return 0;
}
// NOTE: Firefox and Eye of GNOME both interpret multiple arguments
// in a special way. This is problematic, because one-element lists
// are unrepresentable.
// TODO(p): Complain to the user if there's more than one argument.
// Best show the help message, if we can figure that out.
const gchar *path_arg = path_args ? path_args[0] : NULL;
gtk_window_set_default_icon_name(PROJECT_NAME);
const char *style = "fastiv-view, fastiv-browser { background: black; }";
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_data(provider, style, strlen(style), NULL);
gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g.view = g_object_new(FASTIV_TYPE_VIEW, NULL);
gtk_widget_show_all(g.view);
g.browser_scroller = gtk_scrolled_window_new(NULL, NULL);
g.browser = g_object_new(FASTIV_TYPE_BROWSER, NULL);
gtk_widget_set_vexpand(g.browser, TRUE);
gtk_widget_set_hexpand(g.browser, TRUE);
gtk_container_add(GTK_CONTAINER(g.browser_scroller), g.browser);
// TODO(p): Can we not do it here separately?
gtk_widget_show_all(g.browser_scroller);
GtkWidget *stack = gtk_stack_new();
gtk_stack_set_transition_type(
GTK_STACK(stack), GTK_STACK_TRANSITION_TYPE_NONE);
gtk_container_add(GTK_CONTAINER(stack), g.view);
gtk_container_add(GTK_CONTAINER(stack), g.browser_scroller);
g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
g_signal_connect(g.window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
gtk_container_add(GTK_CONTAINER(g.window), stack);
// The references to closures are initially floating and sunk on connect.
GtkAccelGroup *accel_group = gtk_accel_group_new();
gtk_accel_group_connect(accel_group, GDK_KEY_Escape, 0, 0,
g_cclosure_new(G_CALLBACK(gtk_main_quit), NULL, NULL));
gtk_accel_group_connect(accel_group, GDK_KEY_q, 0, 0,
g_cclosure_new(G_CALLBACK(gtk_main_quit), NULL, NULL));
gtk_accel_group_connect(accel_group, GDK_KEY_o, 0, 0,
g_cclosure_new(G_CALLBACK(on_open), NULL, NULL));
gtk_accel_group_connect(accel_group, GDK_KEY_o, GDK_CONTROL_MASK, 0,
g_cclosure_new(G_CALLBACK(on_open), NULL, NULL));
// FIXME: The left/right arrows can't be bound this way at all.
gtk_accel_group_connect(accel_group, GDK_KEY_Left, 0, 0,
g_cclosure_new(G_CALLBACK(on_previous), NULL, NULL));
gtk_accel_group_connect(accel_group, GDK_KEY_Page_Up, 0, 0,
g_cclosure_new(G_CALLBACK(on_previous), NULL, NULL));
gtk_accel_group_connect(accel_group, GDK_KEY_Right, 0, 0,
g_cclosure_new(G_CALLBACK(on_next), NULL, NULL));
gtk_accel_group_connect(accel_group, GDK_KEY_Page_Down, 0, 0,
g_cclosure_new(G_CALLBACK(on_next), NULL, NULL));
gtk_accel_group_connect(accel_group, GDK_KEY_space, 0, 0,
g_cclosure_new(G_CALLBACK(on_next), NULL, NULL));
gtk_window_add_accel_group(GTK_WINDOW(g.window), accel_group);
g.supported_globs = get_supported_globs();
g.files = g_ptr_array_new_full(16, g_free);
gchar *cwd = g_get_current_dir();
// TODO(p): Desired behaviour:
// - No arguments: show directory view of the current working directory.
// - File argument: load its directory for browsing, open the file.
// - Directory argument: load its directory for browsing, show the dir.
GStatBuf st;
if (!path_arg) {
load_directory(cwd);
} else if (g_stat(path_arg, &st)) {
show_error_dialog(g_error_new(G_FILE_ERROR,
g_file_error_from_errno(errno),
"%s: %s", path_arg, g_strerror(errno)));
load_directory(cwd);
} else {
gchar *path_arg_absolute = g_canonicalize_filename(path_arg, cwd);
if (S_ISDIR(st.st_mode))
load_directory(path_arg_absolute);
else
open(path_arg_absolute);
g_free(path_arg_absolute);
}
g_free(cwd);
if (g.files_index < 0)
gtk_stack_set_visible_child(GTK_STACK(stack), g.browser_scroller);
gtk_widget_show_all(g.window);
gtk_main();
return 0;
}

View File

@@ -1,11 +0,0 @@
[Desktop Entry]
Type=Application
Name=fastiv
GenericName=Image Viewer
Icon=fastiv
Exec=fastiv %F
Terminal=false
StartupNotify=true
Categories=Graphics;2DGraphics;Viewer;
# TODO(p): Generate this list from source files.
MimeType=image/png;image/bmp;image/gif;image/jpeg;image/x-dcraw;

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="base-background" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#ffffff" offset="0.1" />
<stop stop-color="#7f7f7f" offset="1" />
</linearGradient>
<linearGradient id="i-background">
<stop stop-color="#e0e0e0" offset="0" />
<stop stop-color="#ffffff" offset="1" />
</linearGradient>
<linearGradient id="red-background" x1="0" y1="0" x2="0" y2="1">
<stop stop-color="#ff9b9b" offset="0" />
<stop stop-color="#ffffff" offset="0.8" />
</linearGradient>
<linearGradient id="green-background" x1="0" y1="0" x2="0" y2="1">
<stop stop-color="#98ff98" offset="0" />
<stop stop-color="#ffffff" offset="0.8" />
</linearGradient>
<linearGradient id="blue-background" x1="0" y1="0" x2="0" y2="1">
<stop stop-color="#9999ff" offset="0" />
<stop stop-color="#ffffff" offset="0.8" />
</linearGradient>
</defs>
<rect x="1" y="1" width="46" height="46" ry="5"
style="fill: url(#base-background)" stroke="#bfbfbf" stroke-width="2" />
<circle cx="13.5" cy="12" r="4"
style="fill: url(#i-background)" stroke="#000000" stroke-width="2" />
<path d="m 9.75,24 0,13 a 3.5 3.5 180 0 0 7.5,0 l 0,-13 a 3.5 3.5 180 0 0 -7.5,0 z"
style="fill: url(#i-background)" stroke="#000000" stroke-width="2" />
<path d="m 22,8.25 6,0 3,3 3,-3 6,0 -9,10 z"
style="fill: url(#red-background)" stroke="#000000" stroke-width="2"
stroke-linejoin="round" />
<path d="m 22,19.5 6,0 3,3 3,-3 6,0 -9,10 z"
style="fill: url(#green-background)" stroke="#000000" stroke-width="2"
stroke-linejoin="round" />
<path d="m 22,30.75 6,0 3,3 3,-3 6,0 -9,10 z"
style="fill: url(#blue-background)" stroke="#000000" stroke-width="2"
stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

12
fiv-browse.desktop Normal file
View File

@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Name=fiv
GenericName=Image Browser
X-GNOME-FullName=fiv Image Browser
Icon=fiv
Exec=fiv --browse -- %u
NoDisplay=true
Terminal=false
StartupNotify=true
Categories=Utility;FileTools;
MimeType=inode/directory;

2041
fiv-browser.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
//
// fastiv-view.h: fast image viewer - view widget
// fiv-browser.h: filesystem browsing widget
//
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2022, 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,14 +17,12 @@
#pragma once
#include "fiv-io-model.h"
#include <gtk/gtk.h>
#define FASTIV_TYPE_VIEW (fastiv_view_get_type())
G_DECLARE_FINAL_TYPE(FastivView, fastiv_view, FASTIV, VIEW, GtkWidget)
#define FIV_TYPE_BROWSER (fiv_browser_get_type())
G_DECLARE_FINAL_TYPE(FivBrowser, fiv_browser, FIV, BROWSER, GtkWidget)
/// Try to open the given file, synchronously, to be displayed by the widget.
gboolean fastiv_view_open(FastivView *self, const gchar *path, GError **error);
// Private, fastiv-io.c
cairo_surface_t *fastiv_io_open(const gchar *path, GError **error);
cairo_surface_t *fastiv_io_lookup_thumbnail(const gchar *target);
GtkWidget *fiv_browser_new(FivIoModel *model);
void fiv_browser_select(FivBrowser *self, const char *uri);

729
fiv-collection.c Normal file
View File

@@ -0,0 +1,729 @@
//
// fiv-collection.c: GVfs extension for grouping arbitrary files together
//
// Copyright (c) 2022, 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 <gio/gio.h>
#include "fiv-collection.h"
static struct {
GFile **files;
gsize files_len;
} g;
gboolean
fiv_collection_uri_matches(const char *uri)
{
static const char prefix[] = FIV_COLLECTION_SCHEME ":";
return !g_ascii_strncasecmp(uri, prefix, sizeof prefix - 1);
}
GFile **
fiv_collection_get_contents(gsize *len)
{
*len = g.files_len;
return g.files;
}
void
fiv_collection_reload(gchar **uris)
{
if (g.files) {
for (gsize i = 0; i < g.files_len; i++)
g_object_unref(g.files[i]);
g_free(g.files);
}
g.files_len = g_strv_length(uris);
g.files = g_malloc0_n(g.files_len + 1, sizeof *g.files);
for (gsize i = 0; i < g.files_len; i++)
g.files[i] = g_file_new_for_uri(uris[i]);
}
// --- Declarations ------------------------------------------------------------
#define FIV_TYPE_COLLECTION_FILE (fiv_collection_file_get_type())
G_DECLARE_FINAL_TYPE(
FivCollectionFile, fiv_collection_file, FIV, COLLECTION_FILE, GObject)
struct _FivCollectionFile {
GObject parent_instance;
gint index; ///< Original index into g.files, or -1
GFile *target; ///< The wrapped file, or NULL for root
gchar *subpath; ///< Any subpath, rooted at the target
};
#define FIV_TYPE_COLLECTION_ENUMERATOR (fiv_collection_enumerator_get_type())
G_DECLARE_FINAL_TYPE(FivCollectionEnumerator, fiv_collection_enumerator, FIV,
COLLECTION_ENUMERATOR, GFileEnumerator)
struct _FivCollectionEnumerator {
GFileEnumerator parent_instance;
gchar *attributes; ///< Attributes to look up
gsize index; ///< Root: index into g.files
GFileEnumerator *subenumerator; ///< Non-root: a wrapped enumerator
};
// --- Enumerator --------------------------------------------------------------
G_DEFINE_TYPE(
FivCollectionEnumerator, fiv_collection_enumerator, G_TYPE_FILE_ENUMERATOR)
static void
fiv_collection_enumerator_finalize(GObject *object)
{
FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(object);
g_free(self->attributes);
g_clear_object(&self->subenumerator);
}
static GFileInfo *
fiv_collection_enumerator_next_file(GFileEnumerator *enumerator,
GCancellable *cancellable, GError **error)
{
FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator);
if (self->subenumerator) {
GFileInfo *info = g_file_enumerator_next_file(
self->subenumerator, cancellable, error);
if (!info)
return NULL;
// TODO(p): Consider discarding certain classes of attributes
// from the results (adjusting "attributes" is generally unreliable).
GFile *target = g_file_enumerator_get_child(self->subenumerator, info);
gchar *target_uri = g_file_get_uri(target);
g_object_unref(target);
g_file_info_set_attribute_string(
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri);
g_free(target_uri);
return info;
}
if (self->index >= g.files_len)
return NULL;
FivCollectionFile *file = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
file->index = self->index;
file->target = g_object_ref(g.files[self->index++]);
GFileInfo *info = g_file_query_info(G_FILE(file), self->attributes,
G_FILE_QUERY_INFO_NONE, cancellable, error);
g_object_unref(file);
return info;
}
static gboolean
fiv_collection_enumerator_close(
GFileEnumerator *enumerator, GCancellable *cancellable, GError **error)
{
FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator);
if (self->subenumerator)
return g_file_enumerator_close(self->subenumerator, cancellable, error);
return TRUE;
}
static void
fiv_collection_enumerator_class_init(FivCollectionEnumeratorClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fiv_collection_enumerator_finalize;
GFileEnumeratorClass *enumerator_class = G_FILE_ENUMERATOR_CLASS(klass);
enumerator_class->next_file = fiv_collection_enumerator_next_file;
enumerator_class->close_fn = fiv_collection_enumerator_close;
}
static void
fiv_collection_enumerator_init(G_GNUC_UNUSED FivCollectionEnumerator *self)
{
}
// --- Proxying GFile implementation -------------------------------------------
static void fiv_collection_file_file_iface_init(GFileIface *iface);
G_DEFINE_TYPE_WITH_CODE(FivCollectionFile, fiv_collection_file, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE(G_TYPE_FILE, fiv_collection_file_file_iface_init))
static void
fiv_collection_file_finalize(GObject *object)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(object);
if (self->target)
g_object_unref(self->target);
g_free(self->subpath);
}
static GFile *
fiv_collection_file_dup(GFile *file)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
if (self->target)
new->target = g_object_ref(self->target);
new->subpath = g_strdup(self->subpath);
return G_FILE(new);
}
static guint
fiv_collection_file_hash(GFile *file)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
guint hash = g_int_hash(&self->index);
if (self->target)
hash ^= g_file_hash(self->target);
if (self->subpath)
hash ^= g_str_hash(self->subpath);
return hash;
}
static gboolean
fiv_collection_file_equal(GFile *file1, GFile *file2)
{
FivCollectionFile *cf1 = FIV_COLLECTION_FILE(file1);
FivCollectionFile *cf2 = FIV_COLLECTION_FILE(file2);
return cf1->index == cf2->index && cf1->target == cf2->target &&
!g_strcmp0(cf1->subpath, cf2->subpath);
}
static gboolean
fiv_collection_file_is_native(G_GNUC_UNUSED GFile *file)
{
return FALSE;
}
static gboolean
fiv_collection_file_has_uri_scheme(
G_GNUC_UNUSED GFile *file, const char *uri_scheme)
{
return !g_ascii_strcasecmp(uri_scheme, FIV_COLLECTION_SCHEME);
}
static char *
fiv_collection_file_get_uri_scheme(G_GNUC_UNUSED GFile *file)
{
return g_strdup(FIV_COLLECTION_SCHEME);
}
static char *
get_prefixed_name(FivCollectionFile *self, const char *name)
{
return g_strdup_printf("%d. %s", self->index + 1, name);
}
static char *
get_target_basename(FivCollectionFile *self)
{
g_return_val_if_fail(self->target != NULL, g_strdup(""));
// The "http" scheme doesn't behave nicely, make something up if needed.
// Foreign roots likewise need to be fixed up for our needs.
gchar *basename = g_file_get_basename(self->target);
if (!basename || *basename == '/') {
g_free(basename);
basename = g_file_get_uri_scheme(self->target);
}
gchar *name = get_prefixed_name(self, basename);
g_free(basename);
return name;
}
static char *
fiv_collection_file_get_basename(GFile *file)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
if (!self->target)
return g_strdup("/");
if (self->subpath)
return g_path_get_basename(self->subpath);
return get_target_basename(self);
}
static char *
fiv_collection_file_get_path(G_GNUC_UNUSED GFile *file)
{
// This doesn't seem to be worth implementing (for compatible targets).
return NULL;
}
static char *
get_unescaped_uri(FivCollectionFile *self)
{
GString *unescaped = g_string_new(FIV_COLLECTION_SCHEME ":/");
if (!self->target)
return g_string_free(unescaped, FALSE);
gchar *basename = get_target_basename(self);
g_string_append(unescaped, basename);
g_free(basename);
if (self->subpath)
g_string_append(g_string_append(unescaped, "/"), self->subpath);
return g_string_free(unescaped, FALSE);
}
static char *
fiv_collection_file_get_uri(GFile *file)
{
gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file));
gchar *uri = g_uri_escape_string(
unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
g_free(unescaped);
return uri;
}
static char *
fiv_collection_file_get_parse_name(GFile *file)
{
gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file));
gchar *parse_name = g_uri_escape_string(
unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH " ", TRUE);
g_free(unescaped);
return parse_name;
}
static GFile *
fiv_collection_file_get_parent(GFile *file)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
if (!self->target)
return NULL;
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
if (self->subpath) {
new->index = self->index;
new->target = g_object_ref(self->target);
if (strchr(self->subpath, '/'))
new->subpath = g_path_get_dirname(self->subpath);
}
return G_FILE(new);
}
static gboolean
fiv_collection_file_prefix_matches(GFile *prefix, GFile *file)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
FivCollectionFile *parent = FIV_COLLECTION_FILE(prefix);
// The root has no parents.
if (!self->target)
return FALSE;
// The root prefixes everything that is not the root.
if (!parent->target)
return TRUE;
if (self->index != parent->index || !self->subpath)
return FALSE;
if (!parent->subpath)
return TRUE;
return g_str_has_prefix(self->subpath, parent->subpath) &&
self->subpath[strlen(parent->subpath)] == '/';
}
// This virtual method seems to be intended for local files only,
// and documentation claims that the result is in filesystem encoding.
// For us, paths are mostly opaque strings of arbitrary encoding, however.
static char *
fiv_collection_file_get_relative_path(GFile *parent, GFile *descendant)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(descendant);
FivCollectionFile *prefix = FIV_COLLECTION_FILE(parent);
if (!fiv_collection_file_prefix_matches(parent, descendant))
return NULL;
g_assert((!prefix->target && self->target) ||
(prefix->target && self->target && self->subpath));
if (!prefix->target) {
gchar *basename = get_target_basename(self);
gchar *path = g_build_path("/", basename, self->subpath, NULL);
g_free(basename);
return path;
}
return prefix->subpath
? g_strdup(self->subpath + strlen(prefix->subpath) + 1)
: g_strdup(self->subpath);
}
static GFile *
get_file_for_path(const char *path)
{
// Skip all initial slashes, making the result relative to the root.
if (!*(path += strspn(path, "/")))
return g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
char *end = NULL;
guint64 i = g_ascii_strtoull(path, &end, 10);
if (i <= 0 || i > g.files_len || *end != '.')
return g_file_new_for_uri("");
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
new->index = --i;
new->target = g_object_ref(g.files[i]);
const char *subpath = strchr(path, '/');
if (subpath && subpath[1])
new->subpath = g_strdup(++subpath);
return G_FILE(new);
}
static GFile *
fiv_collection_file_resolve_relative_path(
GFile *file, const char *relative_path)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
if (!self->target)
return get_file_for_path(relative_path);
gchar *basename = get_target_basename(self);
gchar *root = g_build_path("/", "/", basename, self->subpath, NULL);
g_free(basename);
gchar *canonicalized = g_canonicalize_filename(relative_path, root);
GFile *result = get_file_for_path(canonicalized);
g_free(canonicalized);
return result;
}
static GFile *
get_target_subpathed(FivCollectionFile *self)
{
return self->subpath
? g_file_resolve_relative_path(self->target, self->subpath)
: g_object_ref(self->target);
}
static GFile *
fiv_collection_file_get_child_for_display_name(
GFile *file, const char *display_name, GError **error)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
if (!self->target)
return get_file_for_path(display_name);
// Implementations often redirect to g_file_resolve_relative_path().
// We don't want to go up (and possibly receive a "/" basename),
// nor do we want to skip path elements.
// TODO(p): This should still be implementable, via URI inspection.
if (strchr(display_name, '/')) {
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Display name must not contain path separators");
return NULL;
}
GFile *intermediate = get_target_subpathed(self);
GFile *resolved =
g_file_get_child_for_display_name(intermediate, display_name, error);
g_object_unref(intermediate);
if (!resolved)
return NULL;
// Try to retrieve the display name converted to whatever insanity
// the target might have chosen to encode its paths with.
gchar *converted = g_file_get_basename(resolved);
g_object_unref(resolved);
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
new->index = self->index;
new->target = g_object_ref(self->target);
new->subpath = self->subpath
? g_build_path("/", self->subpath, converted, NULL)
: g_strdup(converted);
g_free(converted);
return G_FILE(new);
}
static GFileEnumerator *
fiv_collection_file_enumerate_children(GFile *file, const char *attributes,
GFileQueryInfoFlags flags, GCancellable *cancellable, GError **error)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
FivCollectionEnumerator *enumerator = g_object_new(
FIV_TYPE_COLLECTION_ENUMERATOR, "container", file, NULL);
enumerator->attributes = g_strdup(attributes);
if (self->target) {
GFile *intermediate = get_target_subpathed(self);
enumerator->subenumerator = g_file_enumerate_children(
intermediate, enumerator->attributes, flags, cancellable, error);
g_object_unref(intermediate);
}
return G_FILE_ENUMERATOR(enumerator);
}
// TODO(p): Implement async variants of this proxying method.
static GFileInfo *
fiv_collection_file_query_info(GFile *file, const char *attributes,
GFileQueryInfoFlags flags, GCancellable *cancellable,
G_GNUC_UNUSED GError **error)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
GError *e = NULL;
if (!self->target) {
GFileInfo *info = g_file_info_new();
g_file_info_set_file_type(info, G_FILE_TYPE_DIRECTORY);
g_file_info_set_name(info, "/");
g_file_info_set_display_name(info, "Collection");
GIcon *icon = g_icon_new_for_string("shapes-symbolic", NULL);
if (icon) {
g_file_info_set_symbolic_icon(info, icon);
g_object_unref(icon);
} else {
g_warning("%s", e->message);
g_error_free(e);
}
return info;
}
// The "http" scheme doesn't behave nicely, make something up if needed.
GFile *intermediate = get_target_subpathed(self);
GFileInfo *info =
g_file_query_info(intermediate, attributes, flags, cancellable, &e);
if (!info) {
g_warning("%s", e->message);
g_error_free(e);
info = g_file_info_new();
g_file_info_set_file_type(info, G_FILE_TYPE_REGULAR);
gchar *basename = g_file_get_basename(intermediate);
g_file_info_set_name(info, basename);
// The display name is "guaranteed to always be set" when queried,
// which is up to implementations.
gchar *safe = g_utf8_make_valid(basename, -1);
g_free(basename);
g_file_info_set_display_name(info, safe);
g_free(safe);
}
gchar *target_uri = g_file_get_uri(intermediate);
g_file_info_set_attribute_string(
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri);
g_free(target_uri);
g_object_unref(intermediate);
// Ensure all basenames that might have been set have the numeric prefix.
const char *name = NULL;
if (!self->subpath) {
// Always set this, because various schemes may not do so themselves,
// which then troubles GFileEnumerator.
gchar *basename = get_target_basename(self);
g_file_info_set_name(info, basename);
g_free(basename);
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 (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);
}
}
return info;
}
static GFileInfo *
fiv_collection_file_query_filesystem_info(G_GNUC_UNUSED GFile *file,
G_GNUC_UNUSED const char *attributes,
G_GNUC_UNUSED GCancellable *cancellable, G_GNUC_UNUSED GError **error)
{
GFileInfo *info = g_file_info_new();
GFileAttributeMatcher *matcher = g_file_attribute_matcher_new(attributes);
if (g_file_attribute_matcher_matches(
matcher, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE)) {
g_file_info_set_attribute_string(
info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, FIV_COLLECTION_SCHEME);
}
if (g_file_attribute_matcher_matches(
matcher, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) {
g_file_info_set_attribute_boolean(
info, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, TRUE);
}
g_file_attribute_matcher_unref(matcher);
return info;
}
static GFile *
fiv_collection_file_set_display_name(G_GNUC_UNUSED GFile *file,
G_GNUC_UNUSED const char *display_name,
G_GNUC_UNUSED GCancellable *cancellable, GError **error)
{
g_set_error_literal(
error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Operation not supported");
return NULL;
}
static GFileInputStream *
fiv_collection_file_read(GFile *file, GCancellable *cancellable, GError **error)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
if (!self->target) {
g_set_error_literal(
error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory");
return NULL;
}
GFile *intermediate = get_target_subpathed(self);
GFileInputStream *stream = g_file_read(intermediate, cancellable, error);
g_object_unref(intermediate);
return stream;
}
static void
on_read(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
GFile *intermediate = G_FILE(source_object);
GTask *task = G_TASK(user_data);
GError *error = NULL;
GFileInputStream *result = g_file_read_finish(intermediate, res, &error);
if (result)
g_task_return_pointer(task, result, g_object_unref);
else
g_task_return_error(task, error);
g_object_unref(task);
}
static void
fiv_collection_file_read_async(GFile *file, int io_priority,
GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
{
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
GTask *task = g_task_new(file, cancellable, callback, user_data);
g_task_set_name(task, __func__);
g_task_set_priority(task, io_priority);
if (!self->target) {
g_task_return_new_error(
task, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory");
g_object_unref(task);
return;
}
GFile *intermediate = get_target_subpathed(self);
g_file_read_async(intermediate, io_priority, cancellable, on_read, task);
g_object_unref(intermediate);
}
static GFileInputStream *
fiv_collection_file_read_finish(
G_GNUC_UNUSED GFile *file, GAsyncResult *res, GError **error)
{
return g_task_propagate_pointer(G_TASK(res), error);
}
static void
fiv_collection_file_file_iface_init(GFileIface *iface)
{
// Required methods that would segfault if unimplemented.
iface->dup = fiv_collection_file_dup;
iface->hash = fiv_collection_file_hash;
iface->equal = fiv_collection_file_equal;
iface->is_native = fiv_collection_file_is_native;
iface->has_uri_scheme = fiv_collection_file_has_uri_scheme;
iface->get_uri_scheme = fiv_collection_file_get_uri_scheme;
iface->get_basename = fiv_collection_file_get_basename;
iface->get_path = fiv_collection_file_get_path;
iface->get_uri = fiv_collection_file_get_uri;
iface->get_parse_name = fiv_collection_file_get_parse_name;
iface->get_parent = fiv_collection_file_get_parent;
iface->prefix_matches = fiv_collection_file_prefix_matches;
iface->get_relative_path = fiv_collection_file_get_relative_path;
iface->resolve_relative_path = fiv_collection_file_resolve_relative_path;
iface->get_child_for_display_name =
fiv_collection_file_get_child_for_display_name;
iface->set_display_name = fiv_collection_file_set_display_name;
// Optional methods.
iface->enumerate_children = fiv_collection_file_enumerate_children;
iface->query_info = fiv_collection_file_query_info;
iface->query_filesystem_info = fiv_collection_file_query_filesystem_info;
iface->read_fn = fiv_collection_file_read;
iface->read_async = fiv_collection_file_read_async;
iface->read_finish = fiv_collection_file_read_finish;
iface->supports_thread_contexts = TRUE;
}
static void
fiv_collection_file_class_init(FivCollectionFileClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fiv_collection_file_finalize;
}
static void
fiv_collection_file_init(FivCollectionFile *self)
{
self->index = -1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static GFile *
get_file_for_uri(G_GNUC_UNUSED GVfs *vfs, const char *identifier,
G_GNUC_UNUSED gpointer user_data)
{
static const char prefix[] = FIV_COLLECTION_SCHEME ":";
const char *path = identifier + sizeof prefix - 1;
if (!g_str_has_prefix(identifier, prefix))
return NULL;
// Specifying the authority is not supported.
if (g_str_has_prefix(path, "//"))
return NULL;
// Otherwise, it needs to look like an absolute path.
if (!g_str_has_prefix(path, "/"))
return NULL;
// TODO(p): Figure out what to do about queries and fragments.
// GDummyFile carries them across level, which seems rather arbitrary.
const char *trailing = strpbrk(path, "?#");
gchar *unescaped = g_uri_unescape_segment(path, trailing, "/");
if (!unescaped)
return NULL;
GFile *result = get_file_for_path(unescaped);
g_free(unescaped);
return result;
}
static GFile *
parse_name(GVfs *vfs, const char *identifier, gpointer user_data)
{
// get_file_for_uri() already parses a superset of URIs.
return get_file_for_uri(vfs, identifier, user_data);
}
void
fiv_collection_register(void)
{
GVfs *vfs = g_vfs_get_default();
if (!g_vfs_register_uri_scheme(vfs, FIV_COLLECTION_SCHEME,
get_file_for_uri, NULL, NULL, parse_name, NULL, NULL))
g_warning(FIV_COLLECTION_SCHEME " scheme registration failed");
}

25
fiv-collection.h Normal file
View File

@@ -0,0 +1,25 @@
//
// fiv-collection.h: GVfs extension for grouping arbitrary files together
//
// Copyright (c) 2022, 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 <gio/gio.h>
#define FIV_COLLECTION_SCHEME "collection"
gboolean fiv_collection_uri_matches(const char *uri);
GFile **fiv_collection_get_contents(gsize *len);
void fiv_collection_reload(gchar **uris);
void fiv_collection_register(void);

573
fiv-context-menu.c Normal file
View File

@@ -0,0 +1,573 @@
//
// fiv-context-menu.c: popup menu
//
// 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.
//
// 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 "fiv-collection.h"
#include "fiv-context-menu.h"
G_DEFINE_QUARK(fiv-context-menu-cancellable-quark, fiv_context_menu_cancellable)
static GtkWidget *
info_start_group(GtkWidget *vbox, const char *group)
{
GtkWidget *label = gtk_label_new(group);
gtk_widget_set_hexpand(label, TRUE);
gtk_widget_set_halign(label, GTK_ALIGN_FILL);
PangoAttrList *attrs = pango_attr_list_new();
pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
gtk_label_set_attributes(GTK_LABEL(label), attrs);
pango_attr_list_unref(attrs);
GtkWidget *grid = gtk_grid_new();
GtkWidget *expander = gtk_expander_new(NULL);
gtk_expander_set_label_widget(GTK_EXPANDER(expander), label);
gtk_expander_set_expanded(GTK_EXPANDER(expander), TRUE);
gtk_container_add(GTK_CONTAINER(expander), grid);
gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
gtk_box_pack_start(GTK_BOX(vbox), expander, FALSE, FALSE, 0);
return grid;
}
static GtkWidget *
info_parse(char *tsv)
{
GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
const char *last_group = NULL;
GtkWidget *grid = NULL;
int line = 1, row = 0;
for (char *nl; (nl = strchr(tsv, '\n')); line++, tsv = ++nl) {
*nl = 0;
if (nl > tsv && nl[-1] == '\r')
nl[-1] = 0;
char *group = tsv, *tag = strchr(group, '\t');
if (!tag) {
g_warning("ExifTool parse error on line %d", line);
continue;
}
*tag++ = 0;
for (char *p = group; *p; p++)
if (*p == '_')
*p = ' ';
char *value = strchr(tag, '\t');
if (!value) {
g_warning("ExifTool parse error on line %d", line);
continue;
}
*value++ = 0;
if (!last_group || strcmp(last_group, group)) {
grid = info_start_group(vbox, (last_group = group));
row = 0;
}
GtkWidget *a = gtk_label_new(tag);
gtk_size_group_add_widget(sg, a);
gtk_label_set_selectable(GTK_LABEL(a), TRUE);
gtk_label_set_xalign(GTK_LABEL(a), 0.);
gtk_grid_attach(GTK_GRID(grid), a, 0, row, 1, 1);
GtkWidget *b = gtk_label_new(value);
gtk_label_set_selectable(GTK_LABEL(b), TRUE);
gtk_label_set_xalign(GTK_LABEL(b), 0.);
gtk_label_set_line_wrap(GTK_LABEL(b), TRUE);
gtk_widget_set_hexpand(b, TRUE);
gtk_grid_attach(GTK_GRID(grid), b, 1, row, 1, 1);
row++;
}
g_object_unref(sg);
return vbox;
}
static GtkWidget *
info_make_bar(const char *message)
{
GtkWidget *info = gtk_info_bar_new();
gtk_info_bar_set_message_type(GTK_INFO_BAR(info), GTK_MESSAGE_WARNING);
GtkWidget *info_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info));
// When the label is made selectable, Escape doesn't work when it has focus.
gtk_container_add(GTK_CONTAINER(info_area), gtk_label_new(message));
return info;
}
static void
info_redirect_error(gpointer dialog, GError *error)
{
// The dialog has been closed and destroyed.
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
g_error_free(error);
return;
}
GtkContainer *content_area =
GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog)));
gtk_container_foreach(content_area, (GtkCallback) gtk_widget_destroy, NULL);
gtk_container_add(content_area, info_make_bar(error->message));
if (g_error_matches(error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) {
gtk_box_pack_start(GTK_BOX(content_area),
gtk_label_new("Please install ExifTool."), TRUE, FALSE, 12);
}
g_error_free(error);
gtk_widget_show_all(GTK_WIDGET(dialog));
}
static gchar *
bytes_to_utf8(GBytes *bytes)
{
gsize length = 0;
gconstpointer data = g_bytes_get_data(bytes, &length);
gchar *utf8 = data ? g_utf8_make_valid(data, length) : g_strdup("");
g_bytes_unref(bytes);
return utf8;
}
static void
on_info_finished(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
GError *error = NULL;
GBytes *bytes_out = NULL, *bytes_err = NULL;
if (!g_subprocess_communicate_finish(
G_SUBPROCESS(source_object), res, &bytes_out, &bytes_err, &error)) {
info_redirect_error(user_data, error);
return;
}
gchar *out = bytes_to_utf8(bytes_out);
gchar *err = bytes_to_utf8(bytes_err);
GtkWidget *dialog = GTK_WIDGET(user_data);
GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
gtk_container_foreach(
GTK_CONTAINER(content_area), (GtkCallback) gtk_widget_destroy, NULL);
GtkWidget *scroller = gtk_scrolled_window_new(NULL, NULL);
gtk_box_pack_start(GTK_BOX(content_area), scroller, TRUE, TRUE, 0);
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(scroller), vbox);
if (*err)
gtk_container_add(GTK_CONTAINER(vbox), info_make_bar(g_strstrip(err)));
GtkWidget *info = info_parse(out);
gtk_style_context_add_class(
gtk_widget_get_style_context(info), "fiv-information");
gtk_box_pack_start(GTK_BOX(vbox), info, TRUE, TRUE, 0);
g_free(out);
g_free(err);
gtk_widget_show_all(dialog);
gtk_widget_grab_focus(scroller);
}
static void
info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
{
int flags = G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE;
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_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;
}
GCancellable *cancellable = g_object_get_qdata(
G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
g_subprocess_communicate_async(
subprocess, bytes_in, cancellable, on_info_finished, dialog);
g_object_unref(subprocess);
}
static void
on_info_loaded(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
gchar *file_data = NULL;
gsize file_len = 0;
GError *error = NULL;
if (!g_file_load_contents_finish(
G_FILE(source_object), res, &file_data, &file_len, NULL, &error)) {
info_redirect_error(user_data, error);
return;
}
GtkWidget *dialog = GTK_WIDGET(user_data);
GBytes *bytes_in = g_bytes_new_take(file_data, file_len);
info_spawn(dialog, "-", bytes_in);
g_bytes_unref(bytes_in);
}
static void
on_info_queried(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
GFile *file = G_FILE(source_object);
GError *error = NULL;
GFileInfo *info = g_file_query_info_finish(file, res, &error);
gboolean cancelled =
error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
g_clear_error(&error);
if (cancelled)
return;
gchar *path = NULL;
const char *target_uri = g_file_info_get_attribute_string(
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
if (target_uri) {
GFile *target = g_file_new_for_uri(target_uri);
path = g_file_get_path(target);
g_object_unref(target);
}
g_object_unref(info);
GtkWidget *dialog = GTK_WIDGET(user_data);
GCancellable *cancellable = g_object_get_qdata(
G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
if (path) {
info_spawn(dialog, path, NULL);
g_free(path);
} else {
g_file_load_contents_async(file, cancellable, on_info_loaded, dialog);
}
}
void
fiv_context_menu_information(GtkWindow *parent, const char *uri)
{
GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
"use-header-bar", TRUE,
"title", "Information",
"transient-for", parent,
"destroy-with-parent", TRUE, NULL);
// When the window closes, we cancel all asynchronous calls.
GCancellable *cancellable = g_cancellable_new();
g_object_set_qdata_full(G_OBJECT(dialog),
fiv_context_menu_cancellable_quark(), cancellable, g_object_unref);
g_signal_connect_swapped(
dialog, "destroy", G_CALLBACK(g_cancellable_cancel), cancellable);
GtkWidget *spinner = gtk_spinner_new();
gtk_spinner_start(GTK_SPINNER(spinner));
gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
spinner, TRUE, TRUE, 12);
gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 800);
gtk_widget_show_all(dialog);
// Mostly to identify URIs with no local path--we pipe these into ExifTool.
GFile *file = g_file_new_for_uri(uri);
gchar *parse_name = g_file_get_parse_name(file);
gtk_header_bar_set_subtitle(
GTK_HEADER_BAR(gtk_dialog_get_header_bar(GTK_DIALOG(dialog))),
parse_name);
g_free(parse_name);
gchar *path = g_file_get_path(file);
if (path) {
info_spawn(dialog, path, NULL);
g_free(path);
} else {
// Several GVfs schemes contain pseudo-symlinks
// that don't give out filesystem paths directly.
g_file_query_info_async(file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable,
on_info_queried, dialog);
}
g_object_unref(file);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct _OpenContext {
GWeakRef window; ///< Parent window for any dialogs
GFile *file; ///< The file in question
gchar *content_type;
GAppInfo *app_info;
} OpenContext;
static void
open_context_finalize(gpointer data)
{
OpenContext *self = data;
g_weak_ref_clear(&self->window);
g_clear_object(&self->app_info);
g_clear_object(&self->file);
g_free(self->content_type);
}
static void
open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
{
g_rc_box_release_full(data, open_context_finalize);
}
static void
show_error_dialog(GtkWindow *parent, GError *error)
{
GtkWidget *dialog =
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_error_free(error);
}
static void
open_context_launch(GtkWidget *widget, OpenContext *self)
{
GdkAppLaunchContext *context =
gdk_display_get_app_launch_context(gtk_widget_get_display(widget));
gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget));
gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time());
GList *files = g_list_append(NULL, self->file);
GError *error = NULL;
if (g_app_info_launch(
self->app_info, files, G_APP_LAUNCH_CONTEXT(context), &error)) {
(void) g_app_info_set_as_last_used_for_type(
self->app_info, self->content_type, NULL);
} else {
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);
}
static void
append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)
{
OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
g_weak_ref_init(&ctx->window, NULL);
ctx->file = g_object_ref(template->file);
ctx->content_type = g_strdup(template->content_type);
ctx->app_info = opener;
// On Linux, this prefers the obsoleted X-GNOME-FullName.
gchar *name =
g_strdup_printf("Open With %s", g_app_info_get_display_name(opener));
// It's documented that we can touch the child, if we want to use markup.
#if 0
GtkWidget *item = gtk_menu_item_new_with_label(name);
#else
// GtkImageMenuItem overrides the toggle_size_request class method
// to get the image shown in the "margin"--too much work to duplicate.
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
GtkWidget *item = gtk_image_menu_item_new_with_label(name);
GIcon *icon = g_app_info_get_icon(opener);
if (icon) {
GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU);
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), image);
gtk_image_menu_item_set_always_show_image(
GTK_IMAGE_MENU_ITEM(item), TRUE);
}
G_GNUC_END_IGNORE_DEPRECATIONS;
#endif
g_free(name);
g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch),
ctx, open_context_unref, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
}
static void
on_chooser_activate(GtkMenuItem *item, gpointer user_data)
{
OpenContext *ctx = user_data;
GtkWindow *window = g_weak_ref_get(&ctx->window);
GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window,
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type);
g_clear_object(&window);
#if 0
// This exists as a concept in mimeapps.list, but GNOME infuriatingly
// infers it from the last used application if missing.
gtk_app_chooser_widget_set_show_default(
GTK_APP_CHOOSER_WIDGET(gtk_app_chooser_dialog_get_widget(
GTK_APP_CHOOSER_DIALOG(dialog))), TRUE);
#endif
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog));
open_context_launch(GTK_WIDGET(item), ctx);
}
gtk_widget_destroy(dialog);
}
static void
on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
{
OpenContext *ctx = user_data;
GtkWindow *window = g_weak_ref_get(&ctx->window);
gchar *uri = g_file_get_uri(ctx->file);
fiv_context_menu_information(window, uri);
g_clear_object(&window);
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)
{
OpenContext *ctx = user_data;
GtkWindow *window = g_weak_ref_get(&ctx->window);
fiv_context_menu_remove(window, ctx->file);
g_clear_object(&window);
}
static gboolean
destroy_widget_idle_source_func(GtkWidget *widget)
{
// The whole menu is deactivated /before/ any item is activated,
// and a destroyed child item will not activate.
gtk_widget_destroy(widget);
return FALSE;
}
GtkMenu *
fiv_context_menu_new(GtkWidget *widget, GFile *file)
{
GFileInfo *info = g_file_query_info(file,
G_FILE_ATTRIBUTE_STANDARD_TYPE
"," G_FILE_ATTRIBUTE_STANDARD_NAME
"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE
"," G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
G_FILE_QUERY_INFO_NONE, NULL, NULL);
if (!info)
return NULL;
GtkWindow *window = NULL;
if (widget && GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget))))
window = GTK_WINDOW(widget);
// This will have no application pre-assigned, for use with GTK+'s dialog.
OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
g_weak_ref_init(&ctx->window, window);
if (!(ctx->content_type = g_strdup(g_file_info_get_content_type(info))))
ctx->content_type = g_content_type_guess(NULL, NULL, 0, NULL);
GFileType type = g_file_info_get_file_type(info);
const char *target_uri = g_file_info_get_attribute_string(
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
ctx->file = target_uri && g_file_has_uri_scheme(file, FIV_COLLECTION_SCHEME)
? g_file_new_for_uri(target_uri)
: g_object_ref(file);
g_object_unref(info);
GAppInfo *default_ =
g_app_info_get_default_for_type(ctx->content_type, FALSE);
GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
GtkWidget *menu = gtk_menu_new();
if (default_) {
append_opener(menu, default_, ctx);
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
}
for (GList *iter = recommended; iter; iter = iter->next) {
if (!default_ || !g_app_info_equal(iter->data, default_))
append_opener(menu, iter->data, ctx);
else
g_object_unref(iter->data);
}
if (recommended) {
g_list_free(recommended);
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
}
for (GList *iter = fallback; iter; iter = iter->next) {
if (!default_ || !g_app_info_equal(iter->data, default_))
append_opener(menu, iter->data, ctx);
else
g_object_unref(iter->data);
}
if (fallback) {
g_list_free(fallback);
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
}
GtkWidget *item = gtk_menu_item_new_with_label("Open With...");
g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate),
ctx, open_context_unref, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
// TODO(p): Can we avoid using the "trash" string constant for this check?
if (!g_file_has_uri_scheme(file, "trash")) {
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
item = gtk_menu_item_new_with_mnemonic("Move to _Trash");
g_signal_connect_data(item, "activate", G_CALLBACK(on_trash_activate),
g_rc_box_acquire(ctx), open_context_unref, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
}
if (type == G_FILE_TYPE_REGULAR) {
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
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);
}
// As per GTK+ 3 Common Questions, 1.5.
g_object_ref_sink(menu);
g_signal_connect_swapped(menu, "deactivate",
G_CALLBACK(g_idle_add), destroy_widget_idle_source_func);
g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL);
gtk_widget_show_all(menu);
return GTK_MENU(menu);
}

22
fiv-context-menu.h Normal file
View File

@@ -0,0 +1,22 @@
//
// fiv-context-menu.h: popup menu
//
// 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.
//
// 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 <gtk/gtk.h>
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);

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

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

742
fiv-io-model.c Normal file
View File

@@ -0,0 +1,742 @@
//
// fiv-io-model.c: filesystem
//
// Copyright (c) 2021 - 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 "fiv-io.h"
#include "fiv-io-model.h"
#include "xdg.h"
GType
fiv_io_model_sort_get_type(void)
{
static gsize guard;
if (g_once_init_enter(&guard)) {
#define XX(name) {FIV_IO_MODEL_SORT_ ## name, \
"FIV_IO_MODEL_SORT_" #name, #name},
static const GEnumValue values[] = {FIV_IO_MODEL_SORTS(XX) {}};
#undef XX
GType type = g_enum_register_static(
g_intern_static_string("FivIoModelSort"), values);
g_once_init_leave(&guard, type);
}
return guard;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
G_DEFINE_BOXED_TYPE(FivIoModelEntry, fiv_io_model_entry,
fiv_io_model_entry_ref, fiv_io_model_entry_unref)
FivIoModelEntry *
fiv_io_model_entry_ref(FivIoModelEntry *self)
{
return g_rc_box_acquire(self);
}
void
fiv_io_model_entry_unref(FivIoModelEntry *self)
{
g_rc_box_release(self);
}
static size_t
entry_strsize(const char *string)
{
if (!string)
return 0;
return strlen(string) + 1;
}
static char *
entry_strappend(char **p, const char *string, size_t size)
{
if (!string)
return NULL;
char *destination = memcpy(*p, string, size);
*p += size;
return destination;
}
// See model_load_attributes for a (superset of a) list of required attributes.
static FivIoModelEntry *
entry_new(GFile *file, GFileInfo *info)
{
gchar *uri = g_file_get_uri(file);
const gchar *target_uri = g_file_info_get_attribute_string(
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
const gchar *display_name = g_file_info_get_display_name(info);
// TODO(p): Make it possible to use g_utf8_collate_key() instead,
// which does not use natural sorting.
gchar *parse_name = g_file_get_parse_name(file);
gchar *collate_key = g_utf8_collate_key_for_filename(parse_name, -1);
g_free(parse_name);
// The entries are immutable. Packing them into the structure
// should help memory usage as well as performance.
size_t size_uri = entry_strsize(uri);
size_t size_target_uri = entry_strsize(target_uri);
size_t size_display_name = entry_strsize(display_name);
size_t size_collate_key = entry_strsize(collate_key);
FivIoModelEntry *entry = g_rc_box_alloc0(sizeof *entry +
size_uri +
size_target_uri +
size_display_name +
size_collate_key);
gchar *p = (gchar *) entry + sizeof *entry;
entry->uri = entry_strappend(&p, uri, size_uri);
entry->target_uri = entry_strappend(&p, target_uri, size_target_uri);
entry->display_name = entry_strappend(&p, display_name, size_display_name);
entry->collate_key = entry_strappend(&p, collate_key, size_collate_key);
entry->filesize = (guint64) g_file_info_get_size(info);
GDateTime *mtime = g_file_info_get_modification_date_time(info);
if (mtime) {
entry->mtime_msec = g_date_time_to_unix(mtime) * 1000 +
g_date_time_get_microsecond(mtime) / 1000;
g_date_time_unref(mtime);
}
g_free(uri);
g_free(collate_key);
return entry;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct _FivIoModel {
GObject parent_instance;
GPatternSpec **supported_patterns;
GFile *directory; ///< Currently loaded directory
GFileMonitor *monitor; ///< "directory" monitoring
GPtrArray *subdirs; ///< "directory" contents
GPtrArray *files; ///< "directory" contents
FivIoModelSort sort_field; ///< How to sort
gboolean sort_descending; ///< Whether to sort in reverse
gboolean filtering; ///< Only show non-hidden, supported
};
G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT)
enum {
PROP_FILTERING = 1,
PROP_SORT_FIELD,
PROP_SORT_DESCENDING,
N_PROPERTIES
};
static GParamSpec *model_properties[N_PROPERTIES];
enum {
RELOADED,
FILES_CHANGED,
SUBDIRECTORIES_CHANGED,
LAST_SIGNAL,
};
// Globals are, sadly, the canonical way of storing signal numbers.
static guint model_signals[LAST_SIGNAL];
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static GPtrArray *
model_entry_array_new(void)
{
return g_ptr_array_new_with_free_func(
(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)
{
gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
if (!utf8)
return FALSE;
gchar *lc = g_utf8_strdown(utf8, -1);
gsize lc_length = strlen(lc);
gchar *reversed = g_utf8_strreverse(lc, lc_length);
g_free(utf8);
// fnmatch() uses the /locale encoding/, and isn't present on Windows.
// TODO(p): Consider using g_file_info_get_display_name() for direct UTF-8.
gboolean result = FALSE;
for (GPatternSpec **p = self->supported_patterns; *p; p++)
if ((result = g_pattern_spec_match(*p, lc_length, lc, reversed)))
break;
g_free(lc);
g_free(reversed);
return result;
}
static inline int
model_compare_entries(FivIoModel *self,
const FivIoModelEntry *entry1, GFile *file1,
const FivIoModelEntry *entry2, GFile *file2)
{
if (g_file_has_prefix(file1, file2))
return +1;
if (g_file_has_prefix(file2, file1))
return -1;
int result = 0;
switch (self->sort_field) {
case FIV_IO_MODEL_SORT_MTIME:
result -= entry1->mtime_msec < entry2->mtime_msec;
result += entry1->mtime_msec > entry2->mtime_msec;
if (result != 0)
break;
// Fall-through
case FIV_IO_MODEL_SORT_NAME:
case FIV_IO_MODEL_SORT_COUNT:
result = strcmp(entry1->collate_key, entry2->collate_key);
}
return self->sort_descending ? -result : +result;
}
static gint
model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
{
const FivIoModelEntry *entry1 = *(const FivIoModelEntry **) a;
const FivIoModelEntry *entry2 = *(const FivIoModelEntry **) b;
GFile *file1 = g_file_new_for_uri(entry1->uri);
GFile *file2 = g_file_new_for_uri(entry2->uri);
int result = model_compare_entries(user_data, entry1, file1, entry2, file2);
g_object_unref(file1);
g_object_unref(file2);
return result;
}
static const char *model_load_attributes =
G_FILE_ATTRIBUTE_STANDARD_TYPE ","
G_FILE_ATTRIBUTE_STANDARD_NAME ","
G_FILE_ATTRIBUTE_STANDARD_SIZE ","
G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
G_FILE_ATTRIBUTE_STANDARD_TARGET_URI ","
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
G_FILE_ATTRIBUTE_TIME_MODIFIED ","
G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC;
static GPtrArray *
model_decide_placement(
FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
{
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;
if (!self->filtering ||
model_supports(self, g_file_info_get_name(info)))
return files;
return NULL;
}
static gboolean
model_reload_to(FivIoModel *self, GFile *directory,
GPtrArray *subdirs, GPtrArray *files, GError **error)
{
if (subdirs)
g_ptr_array_set_size(subdirs, 0);
if (files)
g_ptr_array_set_size(files, 0);
GFileEnumerator *enumerator = g_file_enumerate_children(
directory, model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, error);
if (!enumerator)
return FALSE;
GFileInfo *info = NULL;
GFile *child = NULL;
GError *e = NULL;
while (TRUE) {
if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, &e) &&
e) {
g_warning("%s", e->message);
g_clear_error(&e);
continue;
}
if (!info)
break;
GPtrArray *target =
model_decide_placement(self, info, subdirs, files);
if (target)
g_ptr_array_add(target, entry_new(child, info));
}
g_object_unref(enumerator);
if (subdirs)
g_ptr_array_sort_with_data(subdirs, model_compare, self);
if (files)
g_ptr_array_sort_with_data(files, model_compare, self);
return TRUE;
}
static gboolean
model_reload(FivIoModel *self, GError **error)
{
// Note that this will clear all entries on failure.
gboolean result = model_reload_to(
self, self->directory, self->subdirs, self->files, error);
g_signal_emit(self, model_signals[RELOADED], 0);
return result;
}
static void
model_resort(FivIoModel *self)
{
g_ptr_array_sort_with_data(self->subdirs, model_compare, self);
g_ptr_array_sort_with_data(self->files, model_compare, self);
g_signal_emit(self, model_signals[RELOADED], 0);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gint
model_find(const GPtrArray *target, GFile *file, FivIoModelEntry **entry)
{
for (guint i = 0; i < target->len; i++) {
FivIoModelEntry *e = target->pdata[i];
GFile *f = g_file_new_for_uri(e->uri);
gboolean match = g_file_equal(f, file);
g_object_unref(f);
if (match) {
*entry = e;
return i;
}
}
return -1;
}
enum monitor_event {
MONITOR_NONE,
MONITOR_CHANGING,
MONITOR_RENAMING,
MONITOR_REMOVING,
MONITOR_ADDING
};
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;
else if (!new_entry && index >= 0)
// The file wasn't filtered out but now it is.
event = MONITOR_REMOVING;
if (event == MONITOR_CHANGING) {
fiv_io_model_entry_unref(target->pdata[index]);
target->pdata[index] = fiv_io_model_entry_ref(new_entry);
}
if (event == MONITOR_REMOVING || event == MONITOR_RENAMING)
g_ptr_array_remove_index(target, index);
if (event == MONITOR_RENAMING || event == MONITOR_ADDING)
g_ptr_array_add(target, fiv_io_model_entry_ref(new_entry));
}
static void
on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
GFile *other_file, GFileMonitorEvent event_type, gpointer user_data)
{
FivIoModel *self = user_data;
FivIoModelEntry *old_entry = NULL;
gint files_index = model_find(self->files, file, &old_entry);
gint subdirs_index = model_find(self->subdirs, file, &old_entry);
enum monitor_event event = MONITOR_NONE;
GFile *new_entry_file = NULL;
switch (event_type) {
case G_FILE_MONITOR_EVENT_CHANGED:
case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
event = MONITOR_CHANGING;
new_entry_file = file;
break;
case G_FILE_MONITOR_EVENT_RENAMED:
event = MONITOR_RENAMING;
new_entry_file = other_file;
break;
case G_FILE_MONITOR_EVENT_DELETED:
case G_FILE_MONITOR_EVENT_MOVED_OUT:
event = MONITOR_REMOVING;
break;
case G_FILE_MONITOR_EVENT_CREATED:
case G_FILE_MONITOR_EVENT_MOVED_IN:
event = MONITOR_ADDING;
old_entry = NULL;
new_entry_file = file;
break;
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
// TODO(p): Figure out if we can't make use of _CHANGES_DONE_HINT.
case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
case G_FILE_MONITOR_EVENT_UNMOUNTED:
// TODO(p): Figure out how to handle _UNMOUNTED sensibly.
case G_FILE_MONITOR_EVENT_MOVED:
return;
}
FivIoModelEntry *new_entry = NULL;
GPtrArray *new_target = NULL;
if (new_entry_file) {
GError *error = NULL;
GFileInfo *info = g_file_query_info(new_entry_file,
model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, &error);
if (error) {
g_debug("monitor: %s", error->message);
g_error_free(error);
goto run;
}
if ((new_target =
model_decide_placement(self, info, self->subdirs, self->files)))
new_entry = entry_new(new_entry_file, info);
g_object_unref(info);
if ((files_index != -1 && new_target == self->subdirs) ||
(subdirs_index != -1 && new_target == self->files)) {
g_debug("monitor: ignoring transfer between files and subdirs");
goto out;
}
}
run:
// Keep a reference alive so that signal handlers see the new arrays.
if (old_entry)
fiv_io_model_entry_ref(old_entry);
if (files_index != -1 || new_target == self->files) {
monitor_apply(event, self->files, files_index, new_entry);
g_signal_emit(self, model_signals[FILES_CHANGED],
0, old_entry, new_entry);
}
if (subdirs_index != -1 || new_target == self->subdirs) {
monitor_apply(event, self->subdirs, subdirs_index, new_entry);
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED],
0, old_entry, new_entry);
}
// NOTE: It would make sense to do
// g_ptr_array_sort_with_data(self->{files,subdirs}, model_compare, self);
// but then the iteration behaviour of fiv.c would differ from what's shown
// in the browser. Perhaps we need to use an index-based, fully-synchronized
// interface similar to GListModel::items-changed.
if (old_entry)
fiv_io_model_entry_unref(old_entry);
out:
if (new_entry)
fiv_io_model_entry_unref(new_entry);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// This would be more efficient iteratively, but it's not that important.
static GFile *
model_last_deep_subdirectory(FivIoModel *self, GFile *directory)
{
GFile *result = NULL;
GPtrArray *subdirs = model_entry_array_new();
if (!model_reload_to(self, directory, subdirs, NULL, NULL))
goto out;
if (subdirs->len) {
FivIoModelEntry *entry = g_ptr_array_index(subdirs, subdirs->len - 1);
GFile *last = g_file_new_for_uri(entry->uri);
result = model_last_deep_subdirectory(self, last);
g_object_unref(last);
} else {
result = g_object_ref(directory);
}
out:
g_ptr_array_free(subdirs, TRUE);
return result;
}
GFile *
fiv_io_model_get_previous_directory(FivIoModel *self)
{
g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
GFile *parent_directory = g_file_get_parent(self->directory);
if (!parent_directory)
return NULL;
GFile *result = NULL;
GPtrArray *subdirs = model_entry_array_new();
if (!model_reload_to(self, parent_directory, subdirs, NULL, NULL))
goto out;
for (gsize i = 0; i < subdirs->len; i++) {
FivIoModelEntry *entry = g_ptr_array_index(subdirs, i);
GFile *file = g_file_new_for_uri(entry->uri);
if (g_file_equal(file, self->directory)) {
g_object_unref(file);
break;
}
g_clear_object(&result);
result = file;
}
if (result) {
GFile *last = model_last_deep_subdirectory(self, result);
g_object_unref(result);
result = last;
} else {
result = g_object_ref(parent_directory);
}
out:
g_object_unref(parent_directory);
g_ptr_array_free(subdirs, TRUE);
return result;
}
// This would be more efficient iteratively, but it's not that important.
static GFile *
model_next_directory_within_parents(FivIoModel *self, GFile *directory)
{
GFile *parent_directory = g_file_get_parent(directory);
if (!parent_directory)
return NULL;
GFile *result = NULL;
GPtrArray *subdirs = model_entry_array_new();
if (!model_reload_to(self, parent_directory, subdirs, NULL, NULL))
goto out;
gboolean found_self = FALSE;
for (gsize i = 0; i < subdirs->len; i++) {
FivIoModelEntry *entry = g_ptr_array_index(subdirs, i);
result = g_file_new_for_uri(entry->uri);
if (found_self)
goto out;
found_self = g_file_equal(result, directory);
g_clear_object(&result);
}
if (!result)
result = model_next_directory_within_parents(self, parent_directory);
out:
g_object_unref(parent_directory);
g_ptr_array_free(subdirs, TRUE);
return result;
}
GFile *
fiv_io_model_get_next_directory(FivIoModel *self)
{
g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
if (self->subdirs->len) {
FivIoModelEntry *entry = g_ptr_array_index(self->subdirs, 0);
return g_file_new_for_uri(entry->uri);
}
return model_next_directory_within_parents(self, self->directory);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_model_finalize(GObject *gobject)
{
FivIoModel *self = FIV_IO_MODEL(gobject);
for (GPatternSpec **p = self->supported_patterns; *p; p++)
g_pattern_spec_free(*p);
g_free(self->supported_patterns);
g_clear_object(&self->directory);
g_clear_object(&self->monitor);
g_ptr_array_free(self->subdirs, TRUE);
g_ptr_array_free(self->files, TRUE);
G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject);
}
static void
fiv_io_model_get_property(
GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
{
FivIoModel *self = FIV_IO_MODEL(object);
switch (property_id) {
case PROP_FILTERING:
g_value_set_boolean(value, self->filtering);
break;
case PROP_SORT_FIELD:
g_value_set_enum(value, self->sort_field);
break;
case PROP_SORT_DESCENDING:
g_value_set_boolean(value, self->sort_descending);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static void
fiv_io_model_set_property(
GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
{
FivIoModel *self = FIV_IO_MODEL(object);
switch (property_id) {
case PROP_FILTERING:
if (self->filtering != g_value_get_boolean(value)) {
self->filtering = !self->filtering;
g_object_notify_by_pspec(object, model_properties[property_id]);
(void) model_reload(self, NULL /* error */);
}
break;
case PROP_SORT_FIELD:
if ((int) self->sort_field != g_value_get_enum(value)) {
self->sort_field = g_value_get_enum(value);
g_object_notify_by_pspec(object, model_properties[property_id]);
model_resort(self);
}
break;
case PROP_SORT_DESCENDING:
if (self->sort_descending != g_value_get_boolean(value)) {
self->sort_descending = !self->sort_descending;
g_object_notify_by_pspec(object, model_properties[property_id]);
model_resort(self);
}
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static void
fiv_io_model_class_init(FivIoModelClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->get_property = fiv_io_model_get_property;
object_class->set_property = fiv_io_model_set_property;
object_class->finalize = fiv_io_model_finalize;
model_properties[PROP_FILTERING] = g_param_spec_boolean(
"filtering", "Filtering", "Only show non-hidden, supported entries",
TRUE, G_PARAM_READWRITE);
model_properties[PROP_SORT_FIELD] = g_param_spec_enum(
"sort-field", "Sort field", "Sort order",
FIV_TYPE_IO_MODEL_SORT, FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE);
model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean(
"sort-descending", "Sort descending", "Use reverse sort order",
FALSE, G_PARAM_READWRITE);
g_object_class_install_properties(
object_class, N_PROPERTIES, model_properties);
// All entries might have changed.
model_signals[RELOADED] =
g_signal_new("reloaded", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL, G_TYPE_NONE, 0);
model_signals[FILES_CHANGED] =
g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL,
G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
model_signals[SUBDIRECTORIES_CHANGED] =
g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL,
G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_model_init(FivIoModel *self)
{
self->filtering = TRUE;
char **types = fiv_io_all_supported_media_types();
char **globs = extract_mime_globs((const char **) types);
g_strfreev(types);
gsize n = g_strv_length(globs);
self->supported_patterns =
g_malloc0_n(n + 1, sizeof *self->supported_patterns);
while (n--)
self->supported_patterns[n] = g_pattern_spec_new(globs[n]);
g_strfreev(globs);
self->files = model_entry_array_new();
self->subdirs = model_entry_array_new();
}
gboolean
fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
{
g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE);
g_return_val_if_fail(G_IS_FILE(directory), FALSE);
g_clear_object(&self->directory);
g_clear_object(&self->monitor);
self->directory = g_object_ref(directory);
GError *e = NULL;
if ((self->monitor = g_file_monitor_directory(
directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
g_signal_connect(self->monitor, "changed",
G_CALLBACK(on_monitor_changed), self);
} else {
g_debug("directory monitoring failed: %s", e->message);
g_error_free(e);
}
return model_reload(self, error);
}
GFile *
fiv_io_model_get_location(FivIoModel *self)
{
g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
return self->directory;
}
FivIoModelEntry *const *
fiv_io_model_get_files(FivIoModel *self, gsize *len)
{
*len = self->files->len;
return (FivIoModelEntry *const *) self->files->pdata;
}
FivIoModelEntry *const *
fiv_io_model_get_subdirs(FivIoModel *self, gsize *len)
{
*len = self->subdirs->len;
return (FivIoModelEntry *const *) self->subdirs->pdata;
}

72
fiv-io-model.h Normal file
View File

@@ -0,0 +1,72 @@
//
// fiv-io-model.h: filesystem
//
// Copyright (c) 2021 - 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.
//
#pragma once
#include <gio/gio.h>
#include <glib.h>
// Avoid glib-mkenums.
typedef enum _FivIoModelSort {
#define FIV_IO_MODEL_SORTS(XX) \
XX(NAME) \
XX(MTIME)
#define XX(name) FIV_IO_MODEL_SORT_ ## name,
FIV_IO_MODEL_SORTS(XX)
#undef XX
FIV_IO_MODEL_SORT_COUNT
} FivIoModelSort;
GType fiv_io_model_sort_get_type(void) G_GNUC_CONST;
#define FIV_TYPE_IO_MODEL_SORT (fiv_io_model_sort_get_type())
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct {
const char *uri; ///< GIO URI
const char *target_uri; ///< GIO URI for any target
const char *display_name; ///< Label for the file
const char *collate_key; ///< Collate key for the filename
guint64 filesize; ///< Filesize in bytes
gint64 mtime_msec; ///< Modification time in milliseconds
} FivIoModelEntry;
GType fiv_io_model_entry_get_type(void) G_GNUC_CONST;
#define FIV_TYPE_IO_MODEL_ENTRY (fiv_io_model_entry_get_type())
FivIoModelEntry *fiv_io_model_entry_ref(FivIoModelEntry *self);
void fiv_io_model_entry_unref(FivIoModelEntry *self);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define FIV_TYPE_IO_MODEL (fiv_io_model_get_type())
G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject)
/// Loads a directory. Clears itself even on failure.
gboolean fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error);
/// Returns the current location as a GFile.
/// There is no ownership transfer, and the object may be NULL.
GFile *fiv_io_model_get_location(FivIoModel *self);
/// Returns the previous VFS directory in order, or NULL.
GFile *fiv_io_model_get_previous_directory(FivIoModel *self);
/// Returns the next VFS directory in order, or NULL.
GFile *fiv_io_model_get_next_directory(FivIoModel *self);
FivIoModelEntry *const *fiv_io_model_get_files(FivIoModel *self, gsize *len);
FivIoModelEntry *const *fiv_io_model_get_subdirs(FivIoModel *self, gsize *len);

3614
fiv-io.c Normal file

File diff suppressed because it is too large Load Diff

219
fiv-io.h Normal file
View File

@@ -0,0 +1,219 @@
//
// fiv-io.h: image operations
//
// 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.
//
// 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.
//
#pragma once
#include <cairo.h>
#include <gio/gio.h>
#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.
GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
void fiv_io_profile_free(FivIoProfile *self);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#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 -----------------------------------------------------------------
extern const char *fiv_io_supported_media_types[];
gchar **fiv_io_all_supported_media_types(void);
// 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
};
// TODO(p): Maybe make FivIoProfile a referencable type,
// then loaders could store it in their closures.
struct _FivIoRenderClosure {
/// The rendering is allowed to fail, returning NULL.
FivIoImage *(*render)(
FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
void (*destroy)(FivIoRenderClosure *);
};
// 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
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;
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);
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 ---------------------------------------------
enum { FIV_IO_SERIALIZE_LOW_QUALITY = 1 << 0 };
void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
cairo_surface_t *fiv_io_deserialize(GBytes *bytes, guint64 *user_data);
GBytes *fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error);
// --- Export ------------------------------------------------------------------
/// 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(
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(FivIoImage *page, FivIoImage *frame,
FivIoProfile *target, const char *path, GError **error);

511
fiv-jpegcrop.c Normal file
View File

@@ -0,0 +1,511 @@
//
// fiv-jpegcrop.c: lossless JPEG cropper
//
// Copyright (c) 2022, 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 <gtk/gtk.h>
#include <turbojpeg.h>
#include <stdlib.h>
#include <string.h>
#include "config.h"
// --- Utilities ---------------------------------------------------------------
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
static void
exit_fatal(const char *format, ...)
{
va_list ap;
va_start(ap, format);
gchar *format_nl = g_strdup_printf("%s\n", format);
vfprintf(stderr, format_nl, ap);
free(format_nl);
va_end(ap);
exit(EXIT_FAILURE);
}
// --- Main --------------------------------------------------------------------
struct {
GFile *location;
gchar *data;
gsize len;
int width, height, subsampling, colorspace;
int mcu_width, mcu_height;
cairo_surface_t *surface;
int top, left, right, bottom;
GtkWidget *label;
GtkWidget *window;
GtkWidget *scrolled;
GtkWidget *view;
} g;
static void
show_error_dialog(GError *error)
{
GtkWidget *dialog =
gtk_message_dialog_new(GTK_WINDOW(g.window), GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
g_error_free(error);
}
static gboolean
on_draw(G_GNUC_UNUSED GtkWidget *widget, cairo_t *cr,
G_GNUC_UNUSED gpointer user_data)
{
cairo_set_source_surface(cr, g.surface, 1, 1);
cairo_paint(cr);
cairo_rectangle(cr,
1 + g.left - 0.5,
1 + g.top - 0.5,
g.right - g.left + 1,
g.bottom - g.top + 1);
cairo_set_source_rgb(cr, 1, 1, 1);
cairo_set_line_width(cr, 1);
cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);
cairo_stroke(cr);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
cairo_rectangle(cr, 1, 1, g.width, g.height);
cairo_rectangle(
cr, g.left, g.top, g.right - g.left + 2, g.bottom - g.top + 2);
cairo_clip(cr);
cairo_set_source_rgba(cr, 0, 0, 0, 0.5);
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
cairo_paint(cr);
return TRUE;
}
static GFile *
run_chooser(GtkWidget *dialog)
{
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
gtk_file_chooser_set_local_only(chooser, FALSE);
GtkFileFilter *jpeg = gtk_file_filter_new();
gtk_file_filter_add_mime_type(jpeg, "image/jpeg");
gtk_file_filter_add_pattern(jpeg, "*.jpg");
gtk_file_filter_add_pattern(jpeg, "*.jpeg");
gtk_file_filter_add_pattern(jpeg, "*.jpe");
gtk_file_filter_set_name(jpeg, "JPEG");
gtk_file_chooser_add_filter(chooser, jpeg);
GtkFileFilter *all = gtk_file_filter_new();
gtk_file_filter_add_pattern(all, "*");
gtk_file_filter_set_name(all, "All files");
gtk_file_chooser_add_filter(chooser, all);
GFile *file = NULL;
switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
default:
gtk_widget_destroy(dialog);
// Fall-through.
case GTK_RESPONSE_NONE:
return file;
case GTK_RESPONSE_ACCEPT:
file = gtk_file_chooser_get_file(chooser);
gtk_widget_destroy(dialog);
return file;
}
}
static GFile *
choose_file_to_open(void)
{
GtkWidget *dialog = gtk_file_chooser_dialog_new("Open image",
NULL, GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT, NULL);
return run_chooser(dialog);
}
static GFile *
choose_file_to_save(void)
{
GtkWidget *dialog = gtk_file_chooser_dialog_new("Saved cropped image as",
GTK_WINDOW(g.window), GTK_FILE_CHOOSER_ACTION_SAVE,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Save", GTK_RESPONSE_ACCEPT, NULL);
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
(void) gtk_file_chooser_set_file(chooser, g.location, NULL);
return run_chooser(dialog);
}
static void
on_save_as(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
{
tjhandle h = tjInitTransform();
if (!h) {
show_error_dialog(g_error_new_literal(
G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
return;
}
// Convert up front, because the target is in memory.
tjtransform t = {
.r.x = g.left,
.r.y = g.top,
.r.w = g.right - g.left,
.r.h = g.bottom - g.top,
.op = TJXOP_NONE,
.options = TJXOPT_CROP | TJXOPT_PROGRESSIVE | TJXOPT_PERFECT,
};
guchar *data = NULL;
gulong len = 0;
if (tjTransform(h, (const guchar *) g.data, g.len, 1, &data, &len, &t, 0)) {
show_error_dialog(g_error_new_literal(
G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
goto out;
}
GFile *file = choose_file_to_save();
GError *error = NULL;
if (file &&
!g_file_replace_contents(file, (const char *) data, len, NULL, FALSE,
G_FILE_CREATE_NONE, NULL, NULL, &error)) {
show_error_dialog(error);
goto out;
}
g_clear_object(&file);
tjFree(data);
out:
if (tjDestroy(h)) {
show_error_dialog(g_error_new_literal(
G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
}
}
static void
update_label(void)
{
gchar *text = g_strdup_printf("(%d, %d) × (%d, %d)", g.left, g.top,
g.right - g.left, g.bottom - g.top);
gtk_label_set_label(GTK_LABEL(g.label), text);
g_free(text);
}
static void
update(void)
{
update_label();
gtk_widget_queue_draw(g.view);
}
static void
on_reset(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
{
g.top = 0;
g.left = 0;
g.right = g.width;
g.bottom = g.height;
update();
}
static gboolean
on_mouse(guint state, guint button, gdouble x, gdouble y)
{
if (state != 0)
return FALSE;
switch (button) {
case GDK_BUTTON_PRIMARY:
g.left = CLAMP((int) (x - 1), 0, g.right) / g.mcu_width * g.mcu_width;
g.top = CLAMP((int) (y - 1), 0, g.bottom) / g.mcu_height * g.mcu_height;
update();
return TRUE;
case GDK_BUTTON_SECONDARY:
// Inclusive of pointer position.
g.right = CLAMP(x, g.left, g.width);
g.bottom = CLAMP(y, g.top, g.height);
update();
return TRUE;
default:
return FALSE;
}
}
static gboolean
on_press(G_GNUC_UNUSED GtkWidget *self, GdkEventButton *event,
G_GNUC_UNUSED gpointer user_data)
{
return on_mouse(event->state, event->button, event->x, event->y);
}
static gboolean
on_motion(G_GNUC_UNUSED GtkWidget *self, GdkEventMotion *event,
G_GNUC_UNUSED gpointer user_data)
{
switch (event->state) {
case GDK_BUTTON1_MASK:
return on_mouse(0, GDK_BUTTON_PRIMARY, event->x, event->y);
case GDK_BUTTON3_MASK:
return on_mouse(0, GDK_BUTTON_SECONDARY, event->x, event->y);
}
return FALSE;
}
static void
on_drag_begin(
GtkGestureDrag *self, gdouble start_x, gdouble start_y, gpointer user_data)
{
// The middle mouse button will never be triggered by touch screens,
// so there is only the NULL sequence to care about.
gtk_gesture_set_state(GTK_GESTURE(self), GTK_EVENT_SEQUENCE_CLAIMED);
GdkWindow *window = gtk_widget_get_window(g.view);
GdkCursor *cursor =
gdk_cursor_new_from_name(gdk_window_get_display(window), "grabbing");
gdk_window_set_cursor(window, cursor);
g_object_unref(cursor);
double *last = user_data;
last[0] = start_x;
last[1] = start_y;
}
static void
on_drag_update(GtkGestureDrag *self, gdouble offset_x, gdouble offset_y,
gpointer user_data)
{
double start_x = 0, start_y = 0;
gtk_gesture_drag_get_start_point(self, &start_x, &start_y);
double *last = user_data,
diff_x = (start_x + offset_x) - last[0],
diff_y = (start_y + offset_y) - last[1];
last[0] = start_x + offset_x;
last[1] = start_y + offset_y;
GtkScrolledWindow *sw = GTK_SCROLLED_WINDOW(g.scrolled);
GtkAdjustment *h = gtk_scrolled_window_get_hadjustment(sw);
GtkAdjustment *v = gtk_scrolled_window_get_vadjustment(sw);
if (diff_x)
gtk_adjustment_set_value(h, gtk_adjustment_get_value(h) - diff_x);
if (diff_y)
gtk_adjustment_set_value(v, gtk_adjustment_get_value(v) - diff_y);
}
static void
on_drag_end(G_GNUC_UNUSED GtkGestureDrag *self, G_GNUC_UNUSED gdouble start_x,
G_GNUC_UNUSED gdouble start_y, G_GNUC_UNUSED gpointer user_data)
{
// Cursors follow the widget hierarchy.
gdk_window_set_cursor(gtk_widget_get_window(g.view), NULL);
}
static gboolean
open_jpeg(const char *data, gsize len, GError **error)
{
tjhandle h = tjInitDecompress();
if (!h) {
g_set_error_literal(
error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
return FALSE;
}
if (tjDecompressHeader3(h, (const guint8 *) data, len, &g.width, &g.height,
&g.subsampling, &g.colorspace)) {
g_set_error_literal(
error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
tjDestroy(h);
return FALSE;
}
g.top = 0;
g.left = 0;
g.right = g.width;
g.bottom = g.height;
g.mcu_width = tjMCUWidth[g.subsampling];
g.mcu_height = tjMCUHeight[g.subsampling];
if (tjDestroy(h)) {
g_set_error_literal(
error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
return FALSE;
}
// TODO(p): Eventually, convert to using fiv-io.c directly,
// which will pull in most of fiv's dependencies,
// but also enable correct color management, even for CMYK.
// NOTE: It's possible to include this as a mode of the main binary.
GInputStream *is = g_memory_input_stream_new_from_data(data, len, NULL);
GdkPixbuf *pixbuf = gdk_pixbuf_new_from_stream(is, NULL, error);
g_object_unref(is);
if (!pixbuf)
return FALSE;
const char *orientation = gdk_pixbuf_get_option(pixbuf, "orientation");
if (orientation && strlen(orientation) == 1) {
int n = *orientation - '0';
if (n >= 1 && n <= 8) {
// TODO(p): Apply this to the view, somehow.
}
}
g.surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 1, NULL);
cairo_status_t surface_status = cairo_surface_status(g.surface);
if (surface_status != CAIRO_STATUS_SUCCESS) {
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
cairo_status_to_string(surface_status));
g_clear_pointer(&g.surface, cairo_surface_destroy);
g_object_unref(pixbuf);
return FALSE;
}
return TRUE;
}
int
main(int argc, char *argv[])
{
gboolean show_version = FALSE;
gchar **args = NULL;
const GOptionEntry options[] = {
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
NULL, "[FILE | URI]"},
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
&show_version, "Output version information and exit", NULL},
{},
};
GError *error = NULL;
gboolean initialized = gtk_init_with_args(
&argc, &argv, " - Lossless JPEG cropper", options, NULL, &error);
if (show_version) {
const char *version = PROJECT_VERSION;
printf("%s %s\n", "fiv-jpegcrop", &version[*version == 'v']);
return 0;
}
if (!initialized)
exit_fatal("%s", error->message);
gtk_window_set_default_icon_name(PROJECT_NAME);
// TODO(p): Rather use G_OPTION_ARG_CALLBACK with G_OPTION_FLAG_FILENAME.
// Alternatively, GOptionContext with gtk_get_option_group(TRUE).
// Then we can show the help string here instead (in fiv as well).
if (args && args[1])
exit_fatal("Too many arguments");
else if (args)
g.location = g_file_new_for_commandline_arg(args[0]);
else if (!(g.location = choose_file_to_open()))
exit(EXIT_SUCCESS);
g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
g_signal_connect(g.window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
GFileInfo *info = g_file_query_info(g.location,
G_FILE_ATTRIBUTE_STANDARD_NAME
"," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
G_FILE_QUERY_INFO_NONE, NULL, &error);
if (!info ||
!g_file_load_contents(
g.location, NULL, &g.data, &g.len, NULL, &error) ||
!open_jpeg(g.data, g.len, &error)) {
show_error_dialog(error);
exit(EXIT_FAILURE);
}
GtkWidget *header = gtk_header_bar_new();
gtk_window_set_titlebar(GTK_WINDOW(g.window), header);
gtk_header_bar_set_title(
GTK_HEADER_BAR(header), g_file_info_get_display_name(info));
gtk_header_bar_set_subtitle(GTK_HEADER_BAR(header),
"Use L/R mouse buttons to adjust the crop region.");
gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(header), TRUE);
g.label = gtk_label_new(NULL);
gtk_header_bar_pack_start(GTK_HEADER_BAR(header), g.label);
update_label();
GtkWidget *save = gtk_button_new_from_icon_name(
"document-save-as-symbolic", GTK_ICON_SIZE_BUTTON);
gtk_widget_set_tooltip_text(save, "Save as...");
g_signal_connect(save, "clicked", G_CALLBACK(on_save_as), NULL);
gtk_header_bar_pack_end(GTK_HEADER_BAR(header), save);
GtkWidget *reset = gtk_button_new_with_mnemonic("_Reset");
gtk_widget_set_tooltip_text(reset, "Reset the crop region");
g_signal_connect(reset, "clicked", G_CALLBACK(on_reset), NULL);
gtk_header_bar_pack_end(GTK_HEADER_BAR(header), reset);
g.view = gtk_drawing_area_new();
gtk_widget_set_size_request(g.view, g.width + 2, g.height + 2);
gtk_widget_add_events(g.view,
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
GDK_POINTER_MOTION_MASK);
g_signal_connect(g.view, "draw",
G_CALLBACK(on_draw), NULL);
g_signal_connect(g.view, "button-press-event",
G_CALLBACK(on_press), NULL);
g_signal_connect(g.view, "motion-notify-event",
G_CALLBACK(on_motion), NULL);
g.scrolled = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_overlay_scrolling(
GTK_SCROLLED_WINDOW(g.scrolled), FALSE);
gtk_scrolled_window_set_propagate_natural_width(
GTK_SCROLLED_WINDOW(g.scrolled), TRUE);
gtk_scrolled_window_set_propagate_natural_height(
GTK_SCROLLED_WINDOW(g.scrolled), TRUE);
GtkGesture *drag = gtk_gesture_drag_new(g.scrolled);
gtk_event_controller_set_propagation_phase(
GTK_EVENT_CONTROLLER(drag), GTK_PHASE_CAPTURE);
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_MIDDLE);
double last_drag_point[2] = {};
g_signal_connect(drag, "drag-begin",
G_CALLBACK(on_drag_begin), last_drag_point);
g_signal_connect(drag, "drag-update",
G_CALLBACK(on_drag_update), last_drag_point);
g_signal_connect(drag, "drag-end",
G_CALLBACK(on_drag_end), last_drag_point);
gtk_container_add(GTK_CONTAINER(g.scrolled), g.view);
gtk_container_add(GTK_CONTAINER(g.window), g.scrolled);
gtk_window_set_default_size(GTK_WINDOW(g.window), 800, 600);
gtk_widget_show_all(g.window);
// It probably needs to be realized.
GdkWindow *window = gtk_widget_get_window(g.scrolled);
GdkCursor *cursor =
gdk_cursor_new_from_name(gdk_window_get_display(window), "crosshair");
gdk_window_set_cursor(window, cursor);
g_object_unref(cursor);
gtk_main();
g_free(g.data);
g_object_unref(g.location);
g_object_unref(info);
return 0;
}

11
fiv-jpegcrop.desktop Normal file
View File

@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=fiv JPEG Cropper
GenericName=Lossless JPEG Cropper
Icon=fiv
Exec=fiv-jpegcrop %u
NoDisplay=true
Terminal=false
StartupNotify=true
Categories=Graphics;2DGraphics;
MimeType=image/jpeg;

9
fiv-reverse-search Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh -e
if [ "$#" -ne 2 ]; then
echo "Usage: $0 SEARCH-ENGINE-URI-PREFIX {PATH | URI}" >&2
exit 1
fi
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
| curl --silent --show-error --form 'files[]=@-' https://uguu.se/upload \
| jq --raw-output '.files[] | .url | @uri')"

View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=fiv @NAME@ Reverse Image Search
GenericName=@NAME@ Reverse Image Search
Icon=fiv
Exec=fiv-reverse-search "@URL@" %u
NoDisplay=true
Terminal=false
Categories=Graphics;2DGraphics;
MimeType=image/png;image/bmp;image/gif;image/x-tga;image/jpeg;image/webp;

659
fiv-sidebar.c Normal file
View File

@@ -0,0 +1,659 @@
//
// fiv-sidebar.c: molesting GtkPlacesSidebar
//
// Copyright (c) 2021 - 2022, 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 <gtk/gtk.h>
#include "fiv-collection.h"
#include "fiv-context-menu.h"
#include "fiv-io.h"
#include "fiv-sidebar.h"
struct _FivSidebar {
GtkScrolledWindow parent_instance;
GtkPlacesSidebar *places;
GtkWidget *listbox;
FivIoModel *model;
};
G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW)
G_DEFINE_QUARK(fiv-sidebar-drag-gesture-quark, fiv_sidebar_drag_gesture)
G_DEFINE_QUARK(fiv-sidebar-location-quark, fiv_sidebar_location)
G_DEFINE_QUARK(fiv-sidebar-self-quark, fiv_sidebar_self)
enum {
OPEN_LOCATION,
LAST_SIGNAL,
};
// Globals are, sadly, the canonical way of storing signal numbers.
static guint sidebar_signals[LAST_SIGNAL];
static void
fiv_sidebar_dispose(GObject *gobject)
{
FivSidebar *self = FIV_SIDEBAR(gobject);
if (self->model) {
g_signal_handlers_disconnect_by_data(self->model, self);
g_clear_object(&self->model);
}
G_OBJECT_CLASS(fiv_sidebar_parent_class)->dispose(gobject);
}
static void
fiv_sidebar_realize(GtkWidget *widget)
{
GTK_WIDGET_CLASS(fiv_sidebar_parent_class)->realize(widget);
// Fucking GTK+. With no bookmarks, the revealer takes up space anyway.
FivSidebar *self = FIV_SIDEBAR(widget);
gtk_places_sidebar_set_drop_targets_visible(self->places, TRUE, NULL);
gtk_places_sidebar_set_drop_targets_visible(self->places, FALSE, NULL);
}
static void
fiv_sidebar_class_init(FivSidebarClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->dispose = fiv_sidebar_dispose;
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
widget_class->realize = fiv_sidebar_realize;
// You're giving me no choice, Adwaita.
// Your style is hardcoded to match against the class' CSS name.
// And I need to replicate the internal widget structure.
gtk_widget_class_set_css_name(widget_class, "placessidebar");
// TODO(p): Consider a return value, and using it.
sidebar_signals[OPEN_LOCATION] =
g_signal_new("open-location", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL, G_TYPE_NONE,
2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS);
}
static gboolean
on_rowlabel_query_tooltip(GtkWidget *widget,
G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y,
G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip)
{
GtkLabel *label = GTK_LABEL(widget);
if (!pango_layout_is_ellipsized(gtk_label_get_layout(label)))
return FALSE;
gtk_tooltip_set_text(tooltip, gtk_label_get_text(label));
return TRUE;
}
static gboolean
on_breadcrumb_button_release(
G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
// This also prevents unwanted primary button click handling in GtkListBox.
if (event->x > gdk_window_get_width(event->window) ||
event->y > gdk_window_get_height(event->window))
return TRUE;
guint state = event->state & gtk_accelerator_get_default_mod_mask();
if (event->button != GDK_BUTTON_MIDDLE || state != 0)
return FALSE;
GtkListBoxRow *row = GTK_LIST_BOX_ROW(user_data);
g_signal_emit(g_object_get_qdata(G_OBJECT(row), fiv_sidebar_self_quark()),
sidebar_signals[OPEN_LOCATION], 0,
g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark()),
GTK_PLACES_OPEN_NEW_WINDOW);
return TRUE;
}
static void
on_breadcrumb_gesture_drag_begin(GtkGestureDrag *drag,
G_GNUC_UNUSED gdouble start_x, G_GNUC_UNUSED gdouble start_y,
G_GNUC_UNUSED gpointer user_data)
{
// Touch screen dragging is how you scroll the parent GtkScrolledWindow,
// don't steal that gesture. Moreover, touch screen dragging fails
// in the middle, without ever invoking drag-end.
GtkGesture *gesture = GTK_GESTURE(drag);
if (gdk_device_get_source(
gdk_event_get_source_device(gtk_gesture_get_last_event(
gesture, gtk_gesture_get_last_updated_sequence(gesture)))) ==
GDK_SOURCE_TOUCHSCREEN)
gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
}
static void
on_breadcrumb_gesture_drag_update(GtkGestureDrag *drag,
gdouble offset_x, gdouble offset_y, gpointer user_data)
{
GtkWidget *widget = GTK_WIDGET(user_data);
gdouble start_x = 0, start_y = 0;
if (!gtk_gesture_drag_get_start_point(drag, &start_x, &start_y) ||
!gtk_drag_check_threshold(widget, start_x, start_y,
start_x + offset_x, start_y + offset_y))
return;
GtkGesture *gesture = GTK_GESTURE(drag);
gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_CLAIMED);
GdkEvent *event = gdk_event_copy(gtk_gesture_get_last_event(
gesture, gtk_gesture_get_last_updated_sequence(gesture)));
GtkTargetList *target_list = gtk_target_list_new(NULL, 0);
gtk_target_list_add_uri_targets(target_list, 0);
gtk_drag_begin_with_coordinates(
widget, target_list, GDK_ACTION_LINK, 1, event, start_x, start_y);
gtk_target_list_unref(target_list);
gdk_event_free(event);
}
static void
on_breadcrumb_drag_data_get(G_GNUC_UNUSED GtkWidget *widget,
G_GNUC_UNUSED GdkDragContext *context, GtkSelectionData *selection_data,
G_GNUC_UNUSED guint info, G_GNUC_UNUSED guint time_, gpointer user_data)
{
GtkListBoxRow *row = GTK_LIST_BOX_ROW(user_data);
GFile *location =
g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark());
gchar *uris[] = {g_file_get_uri(location), NULL};
gtk_selection_data_set_uris(selection_data, uris);
g_free(*uris);
}
static void
on_breadcrumb_drag_begin(G_GNUC_UNUSED GtkWidget *widget,
GdkDragContext *context, gpointer user_data)
{
gtk_drag_set_icon_name(context, "inode-directory-symbolic", 0, 0);
gtk_places_sidebar_set_drop_targets_visible(user_data, TRUE, context);
}
static void
on_breadcrumb_drag_end(G_GNUC_UNUSED GtkWidget *widget,
GdkDragContext *context, gpointer user_data)
{
gtk_places_sidebar_set_drop_targets_visible(user_data, FALSE, context);
}
static gboolean
on_breadcrumb_button_press(GtkWidget *widget, GdkEventButton *event,
G_GNUC_UNUSED gpointer user_data)
{
if (!gdk_event_triggers_context_menu((const GdkEvent *) event))
return GDK_EVENT_PROPAGATE;
GFile *location =
g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, location), NULL);
return GDK_EVENT_STOP;
}
static gboolean
on_breadcrumb_popup_menu(GtkWidget *widget, G_GNUC_UNUSED gpointer user_data)
{
GFile *location =
g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
gtk_menu_popup_at_widget(fiv_context_menu_new(widget, location), widget,
GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
return TRUE;
}
static GtkWidget *
create_row(FivSidebar *self, GFile *file, const char *icon_name)
{
GError *error = NULL;
GFileInfo *info =
g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
if (!info) {
g_debug("%s", error->message);
g_error_free(error);
return NULL;
}
const char *name = g_file_info_get_display_name(info);
GtkWidget *rowbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
GtkWidget *rowimage =
gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU);
gtk_style_context_add_class(
gtk_widget_get_style_context(rowimage), "sidebar-icon");
gtk_container_add(GTK_CONTAINER(rowbox), rowimage);
GtkWidget *rowlabel = gtk_label_new(name);
gtk_label_set_ellipsize(GTK_LABEL(rowlabel), PANGO_ELLIPSIZE_END);
gtk_widget_set_has_tooltip(rowlabel, TRUE);
g_signal_connect(rowlabel, "query-tooltip",
G_CALLBACK(on_rowlabel_query_tooltip), NULL);
gtk_style_context_add_class(
gtk_widget_get_style_context(rowlabel), "sidebar-label");
gtk_container_add(GTK_CONTAINER(rowbox), rowlabel);
// The revealer is primarily necessary to match Adwaita CSS rules,
// but it conveniently also has its own GdkWindow to hook events on.
GtkWidget *revealer = gtk_revealer_new();
gtk_widget_add_events(
revealer, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
gtk_revealer_set_reveal_child(
GTK_REVEALER(revealer), TRUE);
gtk_revealer_set_transition_type(
GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_NONE);
gtk_container_add(GTK_CONTAINER(revealer), rowbox);
GtkGesture *drag = gtk_gesture_drag_new(revealer);
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
gtk_event_controller_set_propagation_phase(
GTK_EVENT_CONTROLLER(drag), GTK_PHASE_BUBBLE);
g_object_set_qdata_full(G_OBJECT(revealer),
fiv_sidebar_drag_gesture_quark(),
drag, (GDestroyNotify) g_object_unref);
g_signal_connect(drag, "drag-begin",
G_CALLBACK(on_breadcrumb_gesture_drag_begin), revealer);
g_signal_connect(drag, "drag-update",
G_CALLBACK(on_breadcrumb_gesture_drag_update), revealer);
GtkWidget *row = gtk_list_box_row_new();
g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_location_quark(),
g_object_ref(file), (GDestroyNotify) g_object_unref);
g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_self_quark(),
g_object_ref(self), (GDestroyNotify) g_object_unref);
g_signal_connect(row, "button-press-event",
G_CALLBACK(on_breadcrumb_button_press), NULL);
g_signal_connect(row, "popup-menu",
G_CALLBACK(on_breadcrumb_popup_menu), NULL);
// Drag signals need to be hooked to a widget with its own GdkWindow.
g_signal_connect(revealer, "button-release-event",
G_CALLBACK(on_breadcrumb_button_release), row);
g_signal_connect(revealer, "drag-data-get",
G_CALLBACK(on_breadcrumb_drag_data_get), row);
g_signal_connect(revealer, "drag-begin",
G_CALLBACK(on_breadcrumb_drag_begin), self->places);
g_signal_connect(revealer, "drag-end",
G_CALLBACK(on_breadcrumb_drag_end), self->places);
gtk_container_add(GTK_CONTAINER(row), revealer);
gtk_widget_show_all(row);
g_object_unref(info);
return row;
}
static void
on_update_task(GTask *task, G_GNUC_UNUSED gpointer source_object,
G_GNUC_UNUSED gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
{
g_task_return_boolean(task, TRUE);
}
static void
on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res,
G_GNUC_UNUSED gpointer user_data)
{
FivSidebar *self = FIV_SIDEBAR(source_object);
gtk_places_sidebar_set_location(
self->places, fiv_io_model_get_location(self->model));
}
static void
reload_directories(FivSidebar *self)
{
GFile *location = fiv_io_model_get_location(self->model);
gtk_container_foreach(GTK_CONTAINER(self->listbox),
(GtkCallback) gtk_widget_destroy, NULL);
if (!location)
return;
GFile *iter = g_object_ref(location);
GtkWidget *row = NULL;
while (TRUE) {
GFile *parent = g_file_get_parent(iter);
g_object_unref(iter);
if (!(iter = parent))
break;
if ((row = create_row(self, parent, "go-up-symbolic")))
gtk_list_box_prepend(GTK_LIST_BOX(self->listbox), row);
}
// Other options are "folder-{visiting,open}-symbolic", though the former
// is mildly inappropriate (means: open in another window).
if ((row = create_row(self, location, "circle-filled-symbolic")))
gtk_container_add(GTK_CONTAINER(self->listbox), row);
gsize len = 0;
FivIoModelEntry *const *subdirs =
fiv_io_model_get_subdirs(self->model, &len);
for (gsize i = 0; i < len; i++) {
GFile *file = g_file_new_for_uri(subdirs[i]->uri);
if ((row = create_row(self, file, "go-down-symbolic")))
gtk_container_add(GTK_CONTAINER(self->listbox), row);
g_object_unref(file);
}
}
static void
on_model_subdirectories_changed(G_GNUC_UNUSED FivIoModel *model,
FivIoModelEntry *old, FivIoModelEntry *new, gpointer user_data)
{
FivSidebar *self = FIV_SIDEBAR(user_data);
// TODO(p): Optimize: there's no need to update parent directories.
if (!old || !new || strcmp(old->uri, new->uri))
reload_directories(self);
}
static void
update_location(FivSidebar *self)
{
GFile *location = fiv_io_model_get_location(self->model);
GFile *collection = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/");
gtk_places_sidebar_remove_shortcut(self->places, collection);
if (location && g_file_has_uri_scheme(location, FIV_COLLECTION_SCHEME)) {
// add_shortcut() asynchronously requests GFileInfo, and only fills in
// the new row's "uri" data field once that's finished, resulting in
// the immediate set_location() call below failing to find it.
gtk_places_sidebar_add_shortcut(self->places, collection);
// Queue up a callback using the same mechanism that GFile uses.
GTask *task = g_task_new(self, NULL, on_update_task_done, NULL);
g_task_set_name(task, __func__);
g_task_set_priority(task, G_PRIORITY_LOW);
g_task_run_in_thread(task, on_update_task);
g_object_unref(task);
}
g_object_unref(collection);
gtk_places_sidebar_set_location(self->places, location);
reload_directories(self);
}
static void
on_open_breadcrumb(
G_GNUC_UNUSED GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data)
{
FivSidebar *self = FIV_SIDEBAR(user_data);
GFile *location =
g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark());
g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
location, GTK_PLACES_OPEN_NORMAL);
}
static void
on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,
GtkPlacesOpenFlags flags, gpointer user_data)
{
FivSidebar *self = FIV_SIDEBAR(user_data);
g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags);
// Deselect the item in GtkPlacesSidebar, if unsuccessful.
update_location(self);
}
static void
complete_path(GFile *location, GtkListStore *model)
{
// TODO(p): Do not enter directories unless followed by '/'.
// This information has already been stripped from `location`.
// TODO(p): Try out GFileCompleter.
GFile *parent = G_FILE_TYPE_DIRECTORY ==
g_file_query_file_type(location, G_FILE_QUERY_INFO_NONE, NULL)
? g_object_ref(location)
: g_file_get_parent(location);
if (!parent)
return;
GFileEnumerator *enumerator = g_file_enumerate_children(parent,
G_FILE_ATTRIBUTE_STANDARD_NAME
"," G_FILE_ATTRIBUTE_STANDARD_TYPE
"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
G_FILE_QUERY_INFO_NONE, NULL, NULL);
if (!enumerator)
goto fail_enumerator;
while (TRUE) {
GFileInfo *info = NULL;
GFile *child = NULL;
if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) ||
!info)
break;
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;
gchar *parse_name = g_file_get_parse_name(child);
if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) {
gchar *save = parse_name;
parse_name = g_strdup_printf("%s%c", parse_name, G_DIR_SEPARATOR);
g_free(save);
}
gtk_list_store_insert_with_values(model, NULL, -1, 0, parse_name, -1);
g_free(parse_name);
}
g_object_unref(enumerator);
fail_enumerator:
g_object_unref(parent);
}
static GFile *
resolve_location(FivSidebar *self, const char *text)
{
// Relative paths produce invalid GFile objects with this function.
// And even if they didn't, we have our own root for them.
GFile *file = g_file_parse_name(text);
if (g_file_peek_path(file))
return file;
// Neither branch looks like a particularly good solution.
// Though in general, false positives are preferred over negatives.
#if GLIB_CHECK_VERSION(2, 66, 0)
if (g_uri_is_valid(text, G_URI_FLAGS_PARSE_RELAXED, NULL))
return file;
#else
gchar *scheme = g_uri_parse_scheme(text);
g_free(scheme);
if (scheme)
return file;
#endif
GFile *absolute = g_file_get_child_for_display_name(
fiv_io_model_get_location(self->model), text, NULL);
if (!absolute)
return file;
g_object_unref(file);
return absolute;
}
static void
on_enter_location_changed(GtkEntry *entry, gpointer user_data)
{
FivSidebar *self = FIV_SIDEBAR(user_data);
const char *text = gtk_entry_get_text(entry);
GFile *location = resolve_location(self, text);
// Don't touch the network anywhere around here, URIs are a no-no.
GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(entry));
if (!g_file_peek_path(location) || g_file_query_exists(location, NULL))
gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING);
else
gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING);
// XXX: For some reason, this jumps around with longer lists.
GtkEntryCompletion *completion = gtk_entry_get_completion(entry);
GtkTreeModel *model = gtk_entry_completion_get_model(completion);
gtk_list_store_clear(GTK_LIST_STORE(model));
if (g_file_peek_path(location))
complete_path(location, GTK_LIST_STORE(model));
g_object_unref(location);
}
static void
on_show_enter_location(
G_GNUC_UNUSED GtkPlacesSidebar *sidebar, G_GNUC_UNUSED gpointer user_data)
{
FivSidebar *self = FIV_SIDEBAR(user_data);
GtkWidget *dialog = gtk_dialog_new_with_buttons("Enter location",
GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL |
GTK_DIALOG_USE_HEADER_BAR,
"_Open", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING);
gtk_tree_sortable_set_sort_column_id(
GTK_TREE_SORTABLE(model), 0, GTK_SORT_ASCENDING);
GtkEntryCompletion *completion = gtk_entry_completion_new();
gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
gtk_entry_completion_set_text_column(completion, 0);
// TODO(p): Complete ~ paths so that they start with ~, then we can filter.
gtk_entry_completion_set_match_func(
completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL);
g_object_unref(model);
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_completion(GTK_ENTRY(entry), completion);
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
g_signal_connect(entry, "changed",
G_CALLBACK(on_enter_location_changed), self);
// Can't have it ellipsized and word-wrapped at the same time.
GtkWidget *protocols = gtk_label_new("");
gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);
gtk_label_set_xalign(GTK_LABEL(protocols), 0);
gchar *protos = g_strjoinv(
", ", (gchar **) g_vfs_get_supported_uri_schemes(g_vfs_get_default()));
gchar *label = g_strdup_printf("<i>Available protocols:</i> %s", protos);
g_free(protos);
gtk_label_set_markup(GTK_LABEL(protocols), label);
g_free(label);
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
g_object_set(content, "margin", 12, NULL);
gtk_box_set_spacing(GTK_BOX(content), 6);
gtk_container_add(GTK_CONTAINER(content), entry);
gtk_container_add(GTK_CONTAINER(content), protocols);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
gtk_window_set_default_size(GTK_WINDOW(dialog), 800, -1);
gtk_window_set_geometry_hints(GTK_WINDOW(dialog), NULL,
&(GdkGeometry) {.max_width = G_MAXSHORT, .max_height = -1},
GDK_HINT_MAX_SIZE);
gtk_widget_show_all(dialog);
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
GFile *location = resolve_location(self, text);
g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
location, GTK_PLACES_OPEN_NORMAL);
g_object_unref(location);
}
gtk_widget_destroy(dialog);
g_object_unref(completion);
// Deselect the item in GtkPlacesSidebar, if unsuccessful.
update_location(self);
}
static void
fiv_sidebar_init(FivSidebar *self)
{
// TODO(p): Transplant functionality from the shitty GtkPlacesSidebar.
// We cannot reasonably place any new items within its own GtkListBox,
// so we need to replicate the style hierarchy to some extent.
GtkWidget *places = gtk_places_sidebar_new();
self->places = GTK_PLACES_SIDEBAR(places);
gtk_places_sidebar_set_show_recent(self->places, FALSE);
gtk_places_sidebar_set_show_trash(self->places, FALSE);
gtk_places_sidebar_set_open_flags(self->places,
GTK_PLACES_OPEN_NORMAL | GTK_PLACES_OPEN_NEW_WINDOW);
g_signal_connect(self->places, "open-location",
G_CALLBACK(on_open_location), self);
gint minimum_width = -1;
gtk_widget_get_size_request(places, &minimum_width, NULL);
gtk_widget_set_size_request(places, minimum_width, -1);
gtk_places_sidebar_set_show_enter_location(self->places, TRUE);
g_signal_connect(self->places, "show-enter-location",
G_CALLBACK(on_show_enter_location), self);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->places),
GTK_POLICY_NEVER, GTK_POLICY_NEVER);
self->listbox = gtk_list_box_new();
gtk_list_box_set_selection_mode(
GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);
g_signal_connect(self->listbox, "row-activated",
G_CALLBACK(on_open_breadcrumb), self);
// Fill up what would otherwise be wasted space,
// as it is in the examples of Nautilus and Thunar.
GtkWidget *superbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(
GTK_CONTAINER(superbox), GTK_WIDGET(self->places));
gtk_container_add(
GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
gtk_container_add(
GTK_CONTAINER(superbox), self->listbox);
gtk_container_add(GTK_CONTAINER(self), superbox);
gtk_scrolled_window_set_policy(
GTK_SCROLLED_WINDOW(self), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
GTK_STYLE_CLASS_SIDEBAR);
gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
"fiv");
}
// --- Public interface --------------------------------------------------------
GtkWidget *
fiv_sidebar_new(FivIoModel *model)
{
g_return_val_if_fail(FIV_IS_IO_MODEL(model), NULL);
FivSidebar *self = g_object_new(FIV_TYPE_SIDEBAR, NULL);
// This doesn't work from the init function.
GtkWidget *sidebar_port = gtk_bin_get_child(GTK_BIN(self));
gtk_container_set_focus_hadjustment(GTK_CONTAINER(sidebar_port),
gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(self)));
gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port),
gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self)));
self->model = g_object_ref(model);
g_signal_connect_swapped(self->model, "reloaded",
G_CALLBACK(update_location), self);
g_signal_connect(self->model, "subdirectories-changed",
G_CALLBACK(on_model_subdirectories_changed), self);
return GTK_WIDGET(self);
}
void
fiv_sidebar_show_enter_location(FivSidebar *self)
{
g_return_if_fail(FIV_IS_SIDEBAR(self));
g_signal_emit_by_name(self->places, "show-enter-location");
}

28
fiv-sidebar.h Normal file
View File

@@ -0,0 +1,28 @@
//
// fiv-sidebar.h: molesting GtkPlacesSidebar
//
// Copyright (c) 2021 - 2022, 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.
//
#pragma once
#include "fiv-io-model.h"
#include <gtk/gtk.h>
#define FIV_TYPE_SIDEBAR (fiv_sidebar_get_type())
G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow)
GtkWidget *fiv_sidebar_new(FivIoModel *model);
void fiv_sidebar_show_enter_location(FivSidebar *self);

1134
fiv-thumbnail.c Normal file

File diff suppressed because it is too large Load Diff

75
fiv-thumbnail.h Normal file
View File

@@ -0,0 +1,75 @@
//
// fiv-thumbnail.h: thumbnail management
//
// Copyright (c) 2021 - 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.
//
#pragma once
#include <cairo.h>
#include <gio/gio.h>
#include <glib.h>
// Avoid glib-mkenums.
typedef enum _FivThumbnailSize {
#define FIV_THUMBNAIL_SIZES(XX) \
XX(SMALL, 128, "normal") \
XX(NORMAL, 256, "large") \
XX(LARGE, 512, "x-large") \
XX(HUGE, 1024, "xx-large")
#define XX(name, value, dir) FIV_THUMBNAIL_SIZE_ ## name,
FIV_THUMBNAIL_SIZES(XX)
#undef XX
FIV_THUMBNAIL_SIZE_COUNT,
FIV_THUMBNAIL_SIZE_MIN = 0,
FIV_THUMBNAIL_SIZE_MAX = FIV_THUMBNAIL_SIZE_COUNT - 1
} FivThumbnailSize;
GType fiv_thumbnail_size_get_type(void) G_GNUC_CONST;
#define FIV_TYPE_THUMBNAIL_SIZE (fiv_thumbnail_size_get_type())
typedef struct _FivThumbnailSizeInfo {
int size; ///< Nominal size in pixels
const char *thumbnail_spec_name; ///< thumbnail-spec directory name
} FivThumbnailSizeInfo;
enum { FIV_THUMBNAIL_WIDE_COEFFICIENT = 2 };
extern FivThumbnailSizeInfo fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_COUNT];
/// If non-NULL, indicates a thumbnail of insufficient quality.
extern cairo_user_data_key_t fiv_thumbnail_key_lq;
/// Attempts to extract any low-quality thumbnail from fast targets.
/// If `max_size` is a valid value, the image will be downscaled as appropriate.
cairo_surface_t *fiv_thumbnail_extract(
GFile *target, FivThumbnailSize max_size, GError **error);
/// Generates wide thumbnails of up to the specified size, saves them in cache.
/// Returns the surface used for the maximum size, or an error.
cairo_surface_t *fiv_thumbnail_produce(
GFile *target, FivThumbnailSize max_size, GError **error);
/// Like fiv_thumbnail_produce(), but skips the cache.
cairo_surface_t *fiv_thumbnail_produce_for_search(
GFile *target, FivThumbnailSize max_size, GError **error);
/// Retrieves a thumbnail of the most appropriate quality and resolution
/// for the target file.
cairo_surface_t *fiv_thumbnail_lookup(const char *uri,
gint64 mtime_msec, guint64 filesize, FivThumbnailSize size);
/// Invalidate the wide thumbnail cache. May write to standard streams.
void fiv_thumbnail_invalidate(void);

8
fiv-update-desktop-files.in Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh -e
fiv=${DESTDIR:+$DESTDIR/}'@FIV@'
desktopdir=${DESTDIR:+$DESTDIR/}'@DESKTOPDIR@'
types=$("$fiv" --list-supported-media-types | tr '\n' ';')
for desktop in @DESKTOPS@
do sed -i "s|^MimeType=.*|MimeType=$types|" "$desktopdir"/"$desktop"
done

2043
fiv-view.c Normal file

File diff suppressed because it is too large Load Diff

75
fiv-view.h Normal file
View File

@@ -0,0 +1,75 @@
//
// fiv-view.h: image viewing widget
//
// Copyright (c) 2021 - 2022, 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.
//
#pragma once
#include <gtk/gtk.h>
#define FIV_TYPE_VIEW (fiv_view_get_type())
G_DECLARE_FINAL_TYPE(FivView, fiv_view, FIV, VIEW, GtkWidget)
/// Try to open the given file, synchronously, to be displayed by the widget.
/// The current image is cleared on failure.
gboolean fiv_view_set_uri(FivView *self, const char *uri);
// And this is how you avoid glib-mkenums.
typedef enum _FivViewCommand {
#define FIV_VIEW_COMMANDS(XX) \
XX(FIV_VIEW_COMMAND_RELOAD, "reload") \
\
XX(FIV_VIEW_COMMAND_ROTATE_LEFT, "rotate-left") \
XX(FIV_VIEW_COMMAND_MIRROR, "mirror") \
XX(FIV_VIEW_COMMAND_ROTATE_RIGHT, "rotate-right") \
\
XX(FIV_VIEW_COMMAND_PAGE_FIRST, "page-first") \
XX(FIV_VIEW_COMMAND_PAGE_PREVIOUS, "page-previous") \
XX(FIV_VIEW_COMMAND_PAGE_NEXT, "page-next") \
XX(FIV_VIEW_COMMAND_PAGE_LAST, "page-last") \
\
XX(FIV_VIEW_COMMAND_FRAME_FIRST, "frame-first") \
XX(FIV_VIEW_COMMAND_FRAME_PREVIOUS, "frame-previous") \
XX(FIV_VIEW_COMMAND_FRAME_NEXT, "frame-next") \
/* Going to the end frame makes no sense, wrap around if needed. */ \
XX(FIV_VIEW_COMMAND_TOGGLE_PLAYBACK, "toggle-playback") \
\
XX(FIV_VIEW_COMMAND_TOGGLE_CMS, "toggle-cms") \
XX(FIV_VIEW_COMMAND_TOGGLE_FILTER, "toggle-filter") \
XX(FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD, "toggle-checkerboard") \
XX(FIV_VIEW_COMMAND_TOGGLE_ENHANCE, "toggle-enhance") \
XX(FIV_VIEW_COMMAND_COPY, "copy") \
XX(FIV_VIEW_COMMAND_PRINT, "print") \
XX(FIV_VIEW_COMMAND_SAVE_PAGE, "save-page") \
XX(FIV_VIEW_COMMAND_SAVE_FRAME, "save-frame") \
XX(FIV_VIEW_COMMAND_INFO, "info") \
\
XX(FIV_VIEW_COMMAND_ZOOM_IN, "zoom-in") \
XX(FIV_VIEW_COMMAND_ZOOM_OUT, "zoom-out") \
XX(FIV_VIEW_COMMAND_ZOOM_1, "zoom-1") \
XX(FIV_VIEW_COMMAND_FIT_WIDTH, "fit-width") \
XX(FIV_VIEW_COMMAND_FIT_HEIGHT, "fit-height") \
XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit") \
XX(FIV_VIEW_COMMAND_TOGGLE_FIXATE, "toggle-fixate")
#define XX(constant, name) constant,
FIV_VIEW_COMMANDS(XX)
#undef XX
} FivViewCommand;
GType fiv_view_command_get_type(void) G_GNUC_CONST;
#define FIV_TYPE_VIEW_COMMAND (fiv_view_command_get_type())
/// Execute a user action.
void fiv_view_command(FivView *self, FivViewCommand command);

2665
fiv.c Normal file

File diff suppressed because it is too large Load Diff

11
fiv.desktop Normal file
View File

@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=fiv
GenericName=Image Viewer
X-GNOME-FullName=fiv Image Viewer
Icon=fiv
Exec=fiv -- %U
Terminal=false
StartupNotify=true
Categories=Graphics;2DGraphics;Viewer;
MimeType=image/png;image/bmp;image/gif;image/x-tga;image/jpeg;image/webp;

48
fiv.gschema.xml Normal file
View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<enum id="name.janouch.fiv.thumbnail-size">
<value nick='Small' value='0'/>
<value nick='Normal' value='1'/>
<value nick='Large' value='2'/>
<value nick='Huge' value='3'/>
</enum>
<schema path="/name/janouch/fiv/" id="name.janouch.fiv">
<key name='native-view-window' type='b'>
<default>true</default>
<summary>Use a native window for the view</summary>
<description>
On X11, using native GdkWindows enables use of 30-bit Visuals
(that is, 10 bits per channel), at the cost of disabling
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>
</key>
<key name='show-browser-sidebar' type='b'>
<default>true</default>
<summary>Show the browser's sidebar</summary>
</key>
<key name='show-browser-toolbar' type='b'>
<default>true</default>
<summary>Show a toolbar in the browser view</summary>
</key>
<key name='show-view-toolbar' type='b'>
<default>true</default>
<summary>Show a toolbar in the image view</summary>
</key>
<key name='thumbnail-size' enum='name.janouch.fiv.thumbnail-size'>
<default>'Normal'</default>
<summary>Thumbnail size to assume on start-up</summary>
</key>
</schema>
</schemalist>

11
fiv.manifest Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="fiv" version="1.0.0.0" type="win32" />
<dependency>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Windows.Common-Controls"
version="6.0.0.0" type="win32" processorArchitecture="*"
publicKeyToken="6595b64144ccf1df" language="*" />
</dependentAssembly>
</dependency>
</assembly>

3
fiv.rc Normal file
View File

@@ -0,0 +1,3 @@
#include <windows.h>
LD_ICON ICON "fiv.ico"
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "fiv.manifest"

22
fiv.svg Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="v" x1="0" y1="1" x2="0" y2="0">
<stop stop-color="#f60" offset="0" />
<stop stop-color="#fa0" offset="1" />
</linearGradient>
<filter id="shadow" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0.5" flood-color="#000" />
<feComposite in2="SourceGraphic" operator="out" />
<feGaussianBlur stdDeviation="1.4" />
<feOffset dx="1.4" dy="1.4" />
<feComposite in2="SourceGraphic" operator="atop" />
</filter>
</defs>
<path fill="url(#v)" d="m 2,7 h 44 l -17,39 h -10 z" />
<path d="M 12.5,20.5 h -4 v 5 h 4 v 15 h 9 v -15 h 2 v -5 h -2 v -8 c 0,-4 5,-4 5,0 c 0,6 9,6 9,0 c 0,-12 -23,-12 -23,0 z M 26.5,20.5 h 9 v 20 h -9 z"
fill="#fff" stroke="#000" stroke-width="1" filter="url(#shadow)" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

71
fiv.wxs.in Normal file
View File

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

View File

@@ -1,38 +1,384 @@
project('fastiv', 'c', default_options : ['c_std=gnu99'], version : '0.1.0')
# vim: noet ts=4 sts=4 sw=4:
project('fiv', 'c',
default_options : ['c_std=gnu99', 'warning_level=2'],
version : '1.0.0',
meson_version : '>=0.57')
# TODO(p): Use libraw_r later, when we start parallelizing/preloading.
cc = meson.get_compiler('c')
add_project_arguments(
cc.get_supported_arguments('-Wno-cast-function-type'), language : 'c')
# This, annoyingly, enables the leak sanitizer by default,
# which requires some tuning to not stand in the way.
# Use -Db_sanitize=address,undefined and adjust LSAN_OPTIONS yourself.
#if get_option('buildtype').startswith('debug')
# flags = cc.get_supported_arguments('-fsanitize=address,undefined')
# add_project_arguments(flags, language : ['c'])
# add_project_link_arguments(flags, language : ['c'])
#endif
win32 = host_machine.system() == 'windows'
# The likelihood of this being installed is nearly zero. Enable the wrap.
libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
allow_fallback : true)
lcms2 = dependency('lcms2', required : get_option('lcms2'))
# Note that only libraw_r is thread-safe, but we'll just run it out-of process.
libraw = dependency('libraw', required : get_option('libraw'))
# This is a direct runtime dependency, but its usage may be disabled for images.
librsvg = dependency('librsvg-2.0', required : get_option('librsvg'))
xcursor = dependency('xcursor', required : get_option('xcursor'))
libheif = dependency('libheif', required : get_option('libheif'))
libtiff = dependency('libtiff-4', required : get_option('libtiff'))
# This is a direct dependency of GTK+, but its usage may be disabled for images.
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('libpng', version : '>=1.5.4'),
dependency('libwebp'),
dependency('libwebpdemux'),
dependency('libwebpdecoder', required : false),
dependency('libwebpmux'),
# Wuffs is included as a submodule.
lcms2,
libjpegqs,
libraw,
meson.get_compiler('c').find_library('m', required : false),
librsvg,
xcursor,
libheif,
libtiff,
cc.find_library('m', required : false),
]
# As of writing, no pkg-config file is produced, and the plugin is not installed
# by default. The library can be built statically, but it's a bit of a hassle.
have_lcms2_fast_float = false
if not get_option('lcms2fastfloat').disabled()
lcms2ff = dependency('lcms2_fast_float', required : false)
if not lcms2ff.found()
lcms2ff = cc.find_library(
'lcms2_fast_float', required : get_option('lcms2fastfloat'))
if lcms2ff.found() and not cc.has_header('lcms2_fast_float.h')
error('lcms2_fast_float.h not found')
endif
endif
if lcms2ff.found()
dependencies += lcms2ff
have_lcms2_fast_float = true
endif
endif
# As of writing, the API is unstable, and no pkg-config file is produced.
# Trying to wrap Cargo in Meson is a recipe for pain, so no version pinning.
have_resvg = false
if not get_option('resvg').disabled()
resvg = dependency('resvg', required : false)
if not resvg.found()
resvg = cc.find_library('resvg', required : get_option('resvg'))
if resvg.found() and not cc.has_header('resvg.h')
error('resvg.h not found')
endif
endif
if resvg.found()
dependencies += resvg
have_resvg = true
endif
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', meson.project_version())
conf.set('HAVE_LIBRAW', libraw.found())
configure_file(
output : 'config.h',
configuration : conf,
)
executable('fastiv', 'fastiv.c', 'fastiv-view.c', 'fastiv-io.c',
'fastiv-browser.c',
install : true,
dependencies : [dependencies])
# TODO(p): See fastiv_io_open(), consider optionally integrating this.
gdk_pixbuf = dependency('gdk-pixbuf-2.0', required : false)
if gdk_pixbuf.found()
executable('io-benchmark', 'fastiv-io-benchmark.c', 'fastiv-io.c',
build_by_default : false,
dependencies : [dependencies, gdk_pixbuf])
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)
endif
install_data('fastiv.desktop',
install_dir : get_option('datadir') + '/applications')
install_data('fastiv.svg',
install_dir : get_option('datadir') + '/icons/hicolor/scalable/apps')
conf.set('HAVE_JPEG_QS', libjpegqs.found())
conf.set('HAVE_LCMS2', lcms2.found())
conf.set('HAVE_LCMS2_FAST_FLOAT', have_lcms2_fast_float)
conf.set('HAVE_LIBRAW', libraw.found())
conf.set('HAVE_RESVG', have_resvg)
conf.set('HAVE_LIBRSVG', librsvg.found())
conf.set('HAVE_XCURSOR', xcursor.found())
conf.set('HAVE_LIBHEIF', libheif.found())
conf.set('HAVE_LIBTIFF', libtiff.found())
conf.set('HAVE_GDKPIXBUF', gdkpixbuf.found())
config = vcs_tag(
command : ['git', 'describe', '--always', '--dirty=+'],
input : configure_file(output : 'config.h.in', configuration : conf),
output : 'config.h',
)
rc = []
if win32
windows = import('windows')
rsvg_convert = find_program('rsvg-convert')
icotool = find_program('icotool')
# Meson is brain-dead and retarded, so these PNGs cannot be installed,
# only because they must all have the same name when installed.
# The largest size is mainly for an appropriately sized Windows icon.
icon_png_list = []
foreach size : ['16', '32', '48', '256']
icon_dimensions = size + 'x' + size
icon_png_list += custom_target(icon_dimensions + ' icon',
input : 'fiv.svg',
output : 'fiv.' + icon_dimensions + '.png',
command : [rsvg_convert, '--output', '@OUTPUT@',
'--width', size, '--height', size, '@INPUT@'])
endforeach
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
gnome = import('gnome')
gresources = gnome.compile_resources('resources',
'resources/resources.gresource.xml',
source_dir : 'resources',
c_name : 'resources',
)
tiff_tables = custom_target('tiff-tables.h',
output : 'tiff-tables.h',
input : 'tiff-tables.db',
# 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']
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', gresources, rc, config,
objects : iolib,
dependencies : dependencies,
install : true,
win_subsystem : 'windows',
)
desktops += 'fiv-jpegcrop.desktop'
jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
install : true,
dependencies : [
dependency('gtk+-3.0'),
dependency('libturbojpeg'),
],
win_subsystem : 'windows',
)
if get_option('tools').enabled()
# 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', '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',
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
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)
# 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()
# 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)
foreach xml : xmls
test('xmlwf ' + xml, xmlwf, args : files(xml))
test('xmllint ' + xml, xmllint, args : ['--noout', files(xml)])
endforeach
dfv = find_program('desktop-file-validate', required : false, disabler : true)
foreach desktop : desktops
test(desktop, dfv, args : files(desktop))
endforeach
# Finish the installation.
install_data('fiv.svg',
install_dir : get_option('datadir') / 'icons/hicolor/scalable/apps')
install_subdir('docs',
install_dir : docdir, strip_directory : true)
if not win32
asciidoctor = find_program('asciidoctor', required : false)
a2x = find_program('a2x', required : false)
if not asciidoctor.found() and not a2x.found()
warning('Neither asciidoctor nor a2x were found, ' +
'falling back to a substandard manual page generator')
endif
foreach page : [meson.project_name()]
man_capture = false
if asciidoctor.found()
command = [asciidoctor, '-b', 'manpage',
'-a', 'release-version=' + meson.project_version(),
'-o', '@OUTPUT@', '@INPUT@']
elif a2x.found()
command = [a2x, '--doctype', 'manpage', '--format', 'manpage',
'-a', 'release-version=' + meson.project_version(),
'-D', '@OUTDIR@', '@INPUT@']
else
command = ['env', 'LC_ALL=C',
'asciidoc-release-version=' + meson.project_version(),
'awk', '-f', files('submodules/liberty/tools/asciiman.awk'),
'@INPUT@']
man_capture = true
endif
custom_target('manpage for ' + page,
input : 'docs' / page + '.adoc',
output : page + '.1',
capture : man_capture,
command : command,
install : true,
install_dir : get_option('mandir') / 'man1')
endforeach
foreach desktop : desktops
install_data(desktop,
rename : application_ns + desktop,
install_dir : get_option('datadir') / 'applications')
endforeach
# TODO(p): Consider moving this to /usr/share or /usr/lib.
install_data('fiv-reverse-search',
install_dir : get_option('bindir'))
# As usual, handling generated files in Meson is a fucking pain.
updatable_desktops = [application_ns + 'fiv.desktop']
foreach name, uri : {
'Google' : 'https://lens.google.com/uploadbyurl?url=',
'Bing' : 'https://www.bing.com/images/searchbyimage?cbir=sbi&imgurl=',
'Yandex' : 'https://yandex.com/images/search?rpt=imageview&url=',
'TinEye' : 'https://tineye.com/search?url=',
'SauceNAO' : 'https://saucenao.com/search.php?url=',
'IQDB' : 'https://iqdb.org/?url=',
}
desktop = 'fiv-reverse-search-' + name.to_lower() + '.desktop'
updatable_desktops += application_ns + desktop
test(desktop, dfv, args : configure_file(
input : 'fiv-reverse-search.desktop.in',
output : application_ns + desktop,
configuration : {'NAME' : name, 'URL' : uri},
install : true,
install_dir : get_option('datadir') / 'applications',
))
endforeach
# With gdk-pixbuf, fiv.desktop depends on currently installed modules;
# the package manager needs to be told to run this script as necessary.
dynamic_desktops = gdkpixbuf.found()
updater = configure_file(
input : 'fiv-update-desktop-files.in',
output : 'fiv-update-desktop-files',
configuration : {
'FIV' : get_option('prefix') / get_option('bindir') / exe.name(),
'DESKTOPDIR' : get_option('prefix') /
get_option('datadir') / 'applications',
'DESKTOPS' : ' \\\n\t'.join(updatable_desktops),
},
install : dynamic_desktops,
install_dir : get_option('bindir'))
if not meson.is_cross_build()
meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
endif
# Quick and dirty package generation, lacking dependencies.
packaging = configuration_data({
'name' : meson.project_name(),
'version' : meson.project_version(),
'summary' : 'Image viewer',
'author' : 'Přemysl Eric Janouch',
})
subdir('submodules/liberty/meson/packaging')
elif meson.is_cross_build()
# Note that even compiling /from within MSYS2/ can still be a cross-build.
msys2_root = meson.get_external_property('msys2_root')
meson.add_install_script('msys2-install.sh', msys2_root)
wxs = configure_file(
input : 'fiv.wxs.in',
output : 'fiv.wxs',
configuration : configuration_data({
'ProjectName' : meson.project_name(),
'ProjectVersion' : meson.project_version(),
'ProjectURL' : application_url,
}),
)
msi = meson.project_name() + '-' + meson.project_version() + \
'-' + host_machine.cpu() + '.msi'
custom_target('package',
output : msi,
command : [meson.current_source_dir() / 'msys2-package.sh',
host_machine.cpu(), msi, wxs],
env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
console : true,
build_always_stale : true,
build_by_default : false,
)
# This is the minimum to run targets from msys2-configure.sh builds.
meson.add_devenv({
'WINEPATH' : msys2_root / 'bin',
'XDG_DATA_DIRS' : msys2_root / 'share',
})
endif

View File

@@ -1,2 +1,23 @@
option('tools', type : 'feature', value : 'disabled',
description : 'Build a few extra file inspection tools')
option('lcms2', type : 'feature', value : 'auto',
description : 'Build with Little CMS colour management')
option('lcms2fastfloat', type : 'feature', value : 'auto',
description : 'Build with Little CMS fast float plugin support')
option('libjpegqs', type : 'feature', value : 'auto',
description : 'Build with JPEG Quant Smooth integration')
option('libraw', type : 'feature', value : 'auto',
description : 'Build with RAW support, requires LibRaw')
description : 'Build with raw photo support, requires LibRaw')
option('resvg', type : 'feature', value : 'disabled',
description : 'Build with SVG support via resvg (pre-1.0 unstable API)')
option('librsvg', type : 'feature', value : 'auto',
description : 'Build with SVG support, requires librsvg')
option('xcursor', type : 'feature', value : 'auto',
description : 'Build with Xcursor support, requires libXcursor')
option('libheif', type : 'feature', value : 'auto',
description : 'Build with HEIF/AVIF support, requires libheif')
option('libtiff', type : 'feature', value : 'auto',
description : 'Build with TIFF support, requires libtiff')
option('gdk-pixbuf', type : 'feature', value : 'auto',
description : 'Build with a fallback to the gdk-pixbuf library')

149
msys2-configure.sh Executable file
View File

@@ -0,0 +1,149 @@
#!/bin/sh -e
# 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
#
# 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)"
}
dbsync() {
status Fetching repository DB
[ -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" }
field == "%SHA256SUM%" { path = "*packages/" f["%FILENAME%"]
sub(/\t$/, "", path); print $0, path > "db.sums" } END { flush() }
' > db.tsv
}
fetch() {
status Resolving "$@"
mkdir -p packages
awk -F'\t' 'function get(name, i, a) {
if (visited[name]++ || !(name in filenames)) return
print filenames[name]; split(deps[name], a); for (i in a) get(a[i])
} 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]) }' "$@" | tee db.want | \
while IFS= read -r name
do
status Fetching "$name"
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
done
version=$(curl -# https://exiftool.org/ver.txt)
name=exiftool-$version.tar.gz remotename=Image-ExifTool-$version.tar.gz
status Fetching "$remotename"
[ -f "$name" ] || curl -#o "$name" "https://exiftool.org/$remotename"
ln -sf "$name" exiftool.tar.gz
}
verify() {
status Verifying checksums
sha256sum --ignore-missing --quiet -c db.sums
}
extract() {
status Extracting packages
for subdir in *
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
done
while IFS= read -r name
do bsdtar -xf "packages/$name" --strip-components 1 \
--exclude '*/share/man' --exclude '*/share/doc'
done < db.want
# Don't require Perl, which may not exist anymore on i686:
# https://github.com/msys2/MINGW-packages/pull/20085
if [ -d lib/perl5 ]
then
bsdtar -xf exiftool.tar.gz
mv Image-ExifTool-*/exiftool bin
mv Image-ExifTool-*/lib/* lib/perl5/site_perl
rm -rf Image-ExifTool-*
fi
}
configure() {
# Don't let GLib programs inherit wrong paths from the environment.
export XDG_DATA_DIRS=$msys2_root/share
status Configuring packages
wine64 bin/update-mime-database.exe share/mime
wine64 bin/glib-compile-schemas.exe share/glib-2.0/schemas
wine64 bin/gdk-pixbuf-query-loaders.exe \
> lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
}
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 = '$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 = '$pclibdir'
needs_exe_wrapper = $wrap
[host_machine]
system = 'windows'
cpu_family = '$carch'
cpu = '$carch'
endian = 'little'
EOF
meson setup --buildtype=debugoptimized --prefix=/ \
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
}
sourcedir=$(realpath "${2:-$(dirname "$0")}")
builddir=$(realpath "${1:-builddir}")
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$prefix
mkdir -p "$msys2_root"
cd "$msys2_root"
dbsync
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
setup

77
msys2-install.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/bin/sh -e
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 . || :
# 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 || :
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/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 -- {} +
awk 'function whitelist(binary) {
if (seen[binary]++)
return
delete orphans[binary]
while (("strings -a \"" binary "\" 2>/dev/null" | getline string) > 0)
if (match(string, /[-.+_a-zA-Z0-9]+[.][Dd][Ll][Ll]$/))
whitelist("./" substr(string, RSTART, RLENGTH))
} BEGIN {
while (("find . -type f -path \"./*.[Dd][Ll][Ll]\"" | getline) > 0)
orphans[$0]++
while (("find . -type f -path \"./*.[Ee][Xx][Ee]\"" | getline) > 0)
whitelist($0)
while (("find ./lib -type f -path \"./*.[Dd][Ll][Ll]\"" | getline) > 0)
whitelist($0)
for (library in orphans)
print library
}' | xargs rm --
# Removes unused icons from the Adwaita theme. It could be even more aggressive,
# since it keeps around lots of sizes and all the GTK+ stock icons.
find share/icons/Adwaita -type f | awk 'BEGIN {
while (("grep -aho \"[a-z][a-z-]*\" *.dll *.exe" | getline) > 0)
good[$0] = 1
} /[.](png|svg|cur|ani)$/ {
# Cut out the basename without extensions.
match($0, /[^\/]+$/)
base = substr($0, RSTART)
sub(/[.].+$/, "", base)
# Try matching while cutting off suffixes.
# Disregarding the not-much-used GTK_ICON_LOOKUP_GENERIC_FALLBACK.
while (!(keep = good[base]) &&
sub(/-(ltr|rtl|symbolic)$/, "", base)) {}
if (!keep)
print
}' | xargs rm --
wine64 "$msys2_root"/bin/glib-compile-schemas.exe share/glib-2.0/schemas
# This may speed up program start-up a little bit.
wine64 "$msys2_root"/bin/gtk-update-icon-cache-3.0.exe share/icons/Adwaita

35
msys2-package.sh Executable file
View File

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

5
resources/LICENSE Normal file
View File

@@ -0,0 +1,5 @@
Symbolic icons originate from the Icon Development Kit[0].
The authors have disclaimed most of their rights to the works under CC0 1.0[1].
[0] https://gitlab.gnome.org/Teams/Design/icon-development-kit/
[1] https://creativecommons.org/publicdomain/zero/1.0/

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g fill="#2e3436">
<path d="m 1 1 v 14 h 14 v -14 z m 1 1 h 12 v 12 h -12 z m 0 0"/>
<path d="m 6 11 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -4 -1 h 3 v 10 h -3 z m 0 0"/>
<path d="m 8 3 h 1 v 10 h -1 z m 2 9 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m -1 7 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 0" fill-opacity="0.524444"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -96 -620)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 16 748 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 17 747 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 18 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 16 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 17 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 19 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 136 776 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -256 -756)">
<path d="m 219 758 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g fill="#2e3436">
<path d="m 14 5 v -3 h -3 v 3 z m 0 0"/>
<path d="m 11 8 v -3 h -3 v 3 z m 0 0"/>
<path d="m 14 11 v -3 h -3 v 3 z m 0 0"/>
<path d="m 11 14 v -3 h -3 v 3 z m 0 0"/>
<path d="m 8 11 v -3 h -3 v 3 z m 0 0"/>
<path d="m 5 14 v -3 h -3 v 3 z m 0 0"/>
<path d="m 5 8 v -3 h -3 v 3 z m 0 0"/>
<path d="m 8 5 v -3 h -3 v 3 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<path d="m 8 1 c -3.855469 0 -7 3.144531 -7 7 s 3.144531 7 7 7 s 7 -3.144531 7 -7 s -3.144531 -7 -7 -7 z m 0 0" fill="#2e3436"/>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -580 -480)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 1.980469 2 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 4.28125 4.28125 l 4.3125 -4.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035157 0.550781 -0.25 0.75 l -4.28125 4.28125 l 4.25 4.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -4.28125 -4.28125 l -4.28125 4.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 4.28125 -4.25 l -4.28125 -4.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0" fill="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<path d="m 0 1.007812 h 15 l -6 7 v 6 l -3 2 v -8 z m 0 0" fill="#2e3436"/>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -720 -664)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

107
resources/heal-symbolic.svg Normal file
View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1600 v 1200 h -1600 z"/>
</clipPath>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 16 748 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 17 747 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 18 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 16 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 17 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 19 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<path d="m 8.4375 1.480469 c -0.21875 0.179687 -0.410156 0.410156 -0.546875 0.675781 l -4.65625 9.125 c -0.542969 1.070312 -0.109375 2.386719 0.96875 2.960938 l 0.945313 0.496093 c 1.074218 0.570313 2.371093 0.175781 2.914062 -0.894531 l 4.660156 -9.121094 c 0.542969 -1.070312 0.109375 -2.390625 -0.96875 -2.960937 l -0.945312 -0.496094 c -0.804688 -0.429687 -1.726563 -0.320313 -2.371094 0.214844 z m -1.472656 4.464843 c 0.332031 -0.246093 0.8125 -0.179687 1.0625 0.140626 c 0.253906 0.320312 0.1875 0.789062 -0.140625 1.035156 c -0.332031 0.246094 -0.8125 0.183594 -1.0625 -0.140625 c -0.253907 -0.320313 -0.1875 -0.789063 0.140625 -1.035157 z m -0.992188 1.980469 c 0.328125 -0.246093 0.808594 -0.183593 1.0625 0.136719 c 0.25 0.324219 0.1875 0.792969 -0.144531 1.039062 c -0.328125 0.246094 -0.808594 0.179688 -1.0625 -0.140624 c -0.25 -0.320313 -0.1875 -0.789063 0.144531 -1.035157 z m 3.023438 -1.011719 c 0.328125 -0.246093 0.808594 -0.183593 1.0625 0.140626 c 0.25 0.320312 0.1875 0.789062 -0.144532 1.035156 c -0.328124 0.246094 -0.808593 0.183594 -1.0625 -0.140625 c -0.25 -0.320313 -0.1875 -0.789063 0.144532 -1.035157 z m -0.992188 1.980469 c 0.328125 -0.246093 0.808594 -0.183593 1.0625 0.136719 c 0.25 0.324219 0.183594 0.792969 -0.144531 1.039062 c -0.328125 0.242188 -0.8125 0.179688 -1.0625 -0.140624 c -0.25 -0.320313 -0.1875 -0.789063 0.144531 -1.035157 z m 0 0" fill="#2e3436"/>
<path d="m 3.59375 3 c -0.839844 -0.035156 -1.609375 0.4375 -1.96875 1.28125 l -0.4375 1 c -0.480469 1.128906 0 2.46875 1.09375 3 l 0.875 0.40625 l 2.4375 -4.90625 l -1.15625 -0.5625 c -0.273438 -0.132812 -0.5625 -0.207031 -0.84375 -0.21875 z m 9.28125 4.28125 l -2.46875 4.90625 l 1.21875 0.59375 c 1.09375 0.53125 2.332031 0.066406 2.8125 -1.0625 l 0.4375 -1 c 0.480469 -1.128906 0 -2.46875 -1.09375 -3 z m 0 0" fill="#2e3436" fill-opacity="0.501961"/>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 136 776 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -96 -756)">
<path d="m 219 758 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

150
resources/info-symbolic.svg Normal file
View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<path d="m 7.90625 1 c -3.828125 0.050781 -6.90625 3.171875 -6.90625 7 c 0 3.867188 3.132812 7 7 7 s 7 -3.132812 7 -7 s -3.132812 -7 -7 -7 c -0.03125 0 -0.0625 0 -0.09375 0 z m -0.40625 3 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m -0.5 3 h 2 v 5 h -2 z m 0 0" fill="#2e3436"/>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -660 -222)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

152
resources/pin2-symbolic.svg Normal file
View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<path d="m 14 28 c 0 3.3125 -2.6875 6 -6 6 s -6 -2.6875 -6 -6 s 2.6875 -6 6 -6 s 6 2.6875 6 6 z m 0 0" fill="none" stroke="#2e3436" stroke-linecap="round" stroke-width="2"/>
<path d="m 6.992188 2 l -5.070313 4.992188 l 3.261719 2.539062 c -0.519532 2.046875 0.078125 4.214844 1.566406 5.710938 l 3.742188 -3.742188 l 4.5 4.5 h 1 v -1 l -4.5 -4.5 l 3.746093 -3.742188 c -1.5 -1.496093 -3.679687 -2.089843 -5.730469 -1.5625 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
<path d="m 1.992188 7 l 5 -5" fill="none" stroke="#2e3436" stroke-linecap="square" stroke-width="2"/>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -620 -624)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/">
<file alias="LICENSE">../LICENSE</file>
</gresource>
<gresource prefix="/org/gnome/design/IconLibrary/scalable/actions/">
<file preprocess="xml-stripblanks">text-symbolic.svg</file>
<file preprocess="xml-stripblanks">circle-filled-symbolic.svg</file>
<file preprocess="xml-stripblanks">cross-large-symbolic.svg</file>
<file preprocess="xml-stripblanks">funnel-symbolic.svg</file>
<file preprocess="xml-stripblanks">blend-tool-symbolic.svg</file>
<file preprocess="xml-stripblanks">checkerboard-symbolic.svg</file>
<file preprocess="xml-stripblanks">heal-symbolic.svg</file>
<file preprocess="xml-stripblanks">info-symbolic.svg</file>
<file preprocess="xml-stripblanks">pin2-symbolic.svg</file>
<file preprocess="xml-stripblanks">shapes-symbolic.svg</file>
</gresource>
</gresources>

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<g fill="#2e3436">
<path d="m 5.191406 1.296875 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 l -2.5 2.5 c -0.390625 0.390625 -0.390625 1.023437 0 1.414063 l 2.5 2.5 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2.496094 -2.5 c 0.390625 -0.390626 0.390625 -1.023438 0 -1.414063 z m 0 0"/>
<path d="m 9.984375 12.003906 c 0 1.65625 -1.34375 3 -3 3 c -1.660156 0 -3 -1.34375 -3 -3 s 1.339844 -3 3 -3 c 1.65625 0 3 1.34375 3 3 z m 0 0"/>
<path d="m 11.929688 2.007812 c -0.339844 0.015626 -0.644532 0.203126 -0.8125 0.496094 l -2.320313 4 c -0.386719 0.664063 0.09375 1.5 0.863281 1.5 h 4.644532 c 0.769531 0 1.25 -0.835937 0.863281 -1.5 l -2.320313 -4 c -0.1875 -0.328125 -0.542968 -0.519531 -0.917968 -0.496094 z m 0 0"/>
</g>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -620 -420)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

150
resources/text-symbolic.svg Normal file
View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<path d="m 6 1 l -5 14 h 3 c 1.484375 -4 0.023438 0 1.507812 -4 h 4.984376 l 1.507812 4 h 3 l -5 -14 z m 2 3 l 2.023438 5 h -4 z m 0 0" fill="#2e3436"/>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -56 -640)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

1
submodules/liberty Submodule

Submodule submodules/liberty added at 0e86ffe7c3

View File

@@ -0,0 +1,9 @@
[wrap-file]
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]
libjpegqs = jpegqs_dep

View File

@@ -0,0 +1,4 @@
// This separate directory is necessary for Debian's multiarch with jpeg-turbo,
// because its jpeglib.h cannot perform local inclusion of jconfig.h,
// resulting in it being found within jpeg-quantsmooth and breaking the build.
#include "../libjpegqs.h"

View File

@@ -0,0 +1,45 @@
# vim: noet ts=4 sts=4 sw=4:
project('jpeg-qs', 'c')
add_project_arguments('-DWITH_LOG', language : 'c')
deps = [
dependency('libjpeg'),
meson.get_compiler('c').find_library('m', required : false),
]
if host_machine.cpu_family() == 'x86_64'
jpegqs_avx512 = static_library('jpegqs-avx512', 'libjpegqs.c',
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=avx512',
'-mavx512f', '-mfma', '-DSIMD_AVX512'],
dependencies : deps,
implicit_include_directories : false)
jpegqs_avx2 = static_library('jpegqs-avx2', 'libjpegqs.c',
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=avx2',
'-mavx2', '-mfma', '-DSIMD_AVX2'],
dependencies : deps,
implicit_include_directories : false)
jpegqs_sse2 = static_library('jpegqs-sse2', 'libjpegqs.c',
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=sse2', '-msse2', '-DSIMD_SSE2'],
dependencies : deps,
implicit_include_directories : false)
jpegqs_base = static_library('jpegqs-base', 'libjpegqs.c',
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=base', '-DSIMD_BASE'],
dependencies : deps,
implicit_include_directories : false)
jpegqs_lib = static_library('jpegqs', 'libjpegqs.c',
c_args : ['-DSIMD_SELECT'],
dependencies : deps,
link_with : [jpegqs_base, jpegqs_sse2, jpegqs_avx2, jpegqs_avx512],
implicit_include_directories : false)
else
jpegqs_lib = static_library('jpegqs', 'libjpegqs.c',
c_args : ['-DNO_SIMD'],
dependencies : deps,
implicit_include_directories : false)
endif
jpegqs_dep = declare_dependency(
link_with : jpegqs_lib,
include_directories : include_directories('include'),
)

112
tiff-tables.awk Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/awk -f
BEGIN {
FS = ", *"
print "// Generated by tiff-tables.awk. DO NOT MODIFY."
print ""
print "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
print "#include <stddef.h>"
print "#include <stdint.h>"
print ""
print "struct tiff_value {"
print "\tconst char *name;"
print "\tuint16_t value;"
print "};"
print ""
print "struct tiff_entry {"
print "\tconst char *name;"
print "\tuint16_t tag;"
print "\tstruct tiff_value *values;"
print "};"
print "#endif"
}
{
# Remember and strip consecutive comments.
if (match($0, /#/))
comment[++comments] = substr($0, RSTART + 1)
else if (!/[[:space:]]/)
comments = 0
sub(/#.*$/, "")
sub(/[[:space:]]*$/, "")
}
# Converts arbitrary strings to C identifiers (when running in the C locale).
function identifize(s) {
# Regard parenthesised text as comments.
while (match(s, /[[:space:]]\([^)]+\)/)) {
comment[++comments] = substr(s, RSTART, RLENGTH)
s = substr(s, 1, RSTART - 1) substr(s, RSTART + RLENGTH)
}
# Capitalize words (toupper is POSIX), removing spaces and dashes between.
while (match(s, /[-[:space:]]./)) {
s = substr(s, 1, RSTART - 1) \
toupper(substr(s, RSTART + 1, 1)) \
substr(s, RSTART + RLENGTH)
}
# Replace any remaining non-identifier characters with underscores.
gsub(/[^[:alnum:]]/, "_", s)
return s
}
function flushcomments(prefix, i, acc) {
for (i = 1; i <= comments; i++)
acc = acc prefix comment[i] "\n"
comments = 0
return acc
}
function flushvalues() {
if (values) {
allvalues = allvalues "enum " fieldname " {\n" values "};\n\n"
values = ""
fields = fields "\n\t\t{}\n\t}},"
} else if (fields) {
fields = fields " NULL},"
}
}
function flushsection() {
if (section) {
flushvalues()
print "};\n\n" allvalues "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
print "static struct tiff_entry " \
sectionsnakecase "_entries[] = {" fields "\n\t{}\n};"
print "#endif"
}
}
# Section marker
/^= / {
flushsection()
section = identifize(substr($0, 3))
sectionsnakecase = tolower(substr($0, 3))
gsub(/[^[:alnum:]]/, "_", sectionsnakecase)
fields = ""
allvalues = ""
print "\n" flushcomments("//") "enum {"
}
# Field
section && /^[^\t=]/ {
flushvalues()
fieldname = section "_" identifize($2)
fields = fields "\n\t{\"" $2 "\", " fieldname ","
print flushcomments("\t//") "\t" fieldname " = " $1 ","
}
# Value
section && /^\t/ {
sub(/^\t*/, "")
valuename = fieldname "_" identifize($2)
if (!values)
fields = fields " (struct tiff_value[]) {"
values = values flushcomments("\t//") "\t" valuename " = " $1 ",\n"
fields = fields "\n\t\t{\"" $2 "\", " valuename "},"
}
END {
flushsection()
}

471
tiff-tables.db Normal file
View File

@@ -0,0 +1,471 @@
# Use tiff-tables.awk to produce a C source file from this database.
# Use the Internet Archive should any of these links go down.
#
# TIFF Revision 6.0 (1992)
# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFF6.pdf
#
# TIFF Technical Note 1: TIFF Trees (1993)
# https://download.osgeo.org/libtiff/old/TTN1.ps
#
# DRAFT TIFF Technical Note 2 (1995)
# https://www.awaresystems.be/imaging/tiff/specification/TIFFTechNote2.txt
#
# Adobe PageMaker 6.0 TIFF Technical Notes (1995) [includes TTN1]
# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFPM6.pdf
#
# Adobe Photoshop TIFF Technical Notes (2002)
# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFphotoshop.pdf
# https://www.alternatiff.com/resources/TIFFphotoshop.pdf
# - Note that ImageSourceData 8BIM frames are specified differently
# from how Adobe XMP Specification Part 3 defines them.
# - The document places a condition on SubIFDs, without further explanation.
#
# Adobe Photoshop TIFF Technical Note 3 (2005)
# http://chriscox.org/TIFFTN3d1.pdf
#
# Exif Version 2.3 (2012)
# https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf
#
# Exif Version 2.32 (2019)
# https://www.cipa.jp/e/std/std-sec.html
#
# ISO/DIS 12234-2 (TIFF/EP) (2000-06-21)
# http://www.barrypearson.co.uk/top2009/downloads/TAG2000-22_DIS12234-2.pdf
#
# Digital Negative (DNG) Specification 1.5.0.0 (2019)
# https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.5.0.0.pdf
#
# CIPA DC-007-2021 (Multi-Picture Format)
# https://www.cipa.jp/e/std/std-sec.html
# TIFF 6.0
= TIFF
254, NewSubfileType
255, SubfileType
1, Full-resolution image data
2, Reduced-resolution image data
3, Page of a multi-page image
256, ImageWidth
257, ImageLength
258, BitsPerSample
259, Compression
1, Uncompressed
2, CCITT 1D
3, Group 3 Fax
4, Group 4 Fax
5, LZW
6, JPEG
7, JPEG datastream # DRAFT TIFF Technical Note 2 + TIFFphotoshop.pdf
8, Deflate/zlib # Adobe Photoshop TIFF Technical Notes
32773, PackBits
32946, Deflate # Adobe Photoshop TIFF Technical Notes
262, PhotometricInterpretation
0, WhiteIsZero
1, BlackIsZero
2, RGB
3, RGB Palette
4, Transparency mask
5, CMYK
6, YCbCr
8, CIELab
9, ICCLab # Adobe PageMaker 6.0 TIFF Technical Notes
32803, Color filter array # DIS/ISO 12234-2 + DNG 1.5.0.0
34892, LinearRaw # DNG 1.5.0.0
263, Threshholding
1, No dithering or halftoning
2, Ordered dither or halftoning
3, Randomized process
264, CellWidth
265, CellLength
266, FillOrder
1, MSB-first
2, LSB-first
269, DocumentName
270, ImageDescription
271, Make
272, Model
273, StripOffsets
274, Orientation
1, TopLeft
2, TopRight
3, BottomRight
4, BottomLeft
5, LeftTop
6, RightTop
7, RightBottom
8, LeftBottom
277, SamplesPerPixel
278, RowsPerStrip
279, StripByteCounts
280, MinSampleValue
281, MaxSampleValue
282, XResolution
283, YResolution
284, PlanarConfiguration
1, Chunky
2, Planar
285, PageName
286, XPosition
287, YPosition
288, FreeOffsets
289, FreeByteCounts
290, GrayResponseUnit
1, 1/10
2, 1/100
3, 1/1000
4, 1/10000
5, 1/100000
291, GrayResponseCurve
292, T4Options
293, T6Options
296, ResolutionUnit
1, None
2, Inch
3, Centimeter
297, PageNumber
301, TransferFunction
305, Software
306, DateTime
315, Artist
316, HostComputer
317, Predictor
1, None
2, Horizontal
3, Floating point # Adobe Photoshop TIFF Technical Note 3
318, WhitePoint
319, PrimaryChromaticities
320, ColorMap
321, HalftoneHints
322, TileWidth
323, TileLength
324, TileOffsets
325, TileByteCounts
330, SubIFDs # TIFF Technical Note 1: TIFF Trees
332, InkSet
1, CMYK
2, Non-CMYK
333, InkNames
334, NumberOfInks
336, DotRange
337, TargetPrinter
338, ExtraSamples
0, Unspecified
1, Associated alpha
2, Unassociated alpha
339, SampleFormat
1, Unsigned integer
2, Two's complement signed integer
3, IEEE floating-point
4, Undefined
340, SMinSampleValue
341, SMaxSampleValue
342, TransferRange
343, ClipPath # TIFF Technical Note 2: Clipping Path
344, XClipPathUnits # TIFF Technical Note 2: Clipping Path
345, YClipPathUnits # TIFF Technical Note 2: Clipping Path
346, Indexed # TIFF Technical Note 3: Indexed Images
347, JPEGTables # DRAFT TIFF Technical Note 2 + TIFFphotoshop.pdf
351, OPIProxy # Adobe PageMaker 6.0 TIFF Technical Notes
512, JPEGProc
1, Baseline sequential
14, Lossless Huffman
513, JPEGInterchangeFormat
514, JPEGInterchangeFormatLength
515, JPEGRestartInterval
517, JPEGLosslessPredictors
1, A
2, B
3, C
4, A+B+C
5, A+((B-C)/2)
6, B+((A-C)/2)
7, (A+B)/2
518, JPEGPointTransforms
519, JPEGQTables
520, JPEGDCTables
521, JPEGACTables
529, YCbCrCoefficients
530, YCbCrSubSampling
531, YCbCrPositioning
1, Centered
2, Co-sited
532, ReferenceBlackWhite
700, XMP # Adobe XMP Specification Part 3 Table 12/13/39
32781, ImageID # Adobe PageMaker 6.0 TIFF Technical Notes
33421, CFARepeatPatternDim # DIS/ISO 12234-2
33422, CFAPattern # DIS/ISO 12234-2
33423, BatteryLevel # DIS/ISO 12234-2
33432, Copyright
# TODO(p): Extract IPTC DataSets, like we do directly with PSIRs.
33723, IPTC # Adobe XMP Specification Part 3 Table 12/39
# TODO(p): Extract PSIRs, like we do directly with the JPEG segment.
34377, Photoshop # Adobe XMP Specification Part 3 Table 12/39
34665, Exif IFD Pointer # Exif 2.3
34853, GPS Info IFD Pointer # Exif 2.3
37398, TIFF/EP StandardID # DIS/ISO 12234-2
37399, SensingMethod # DIS/ISO 12234-2, similar to Exif 41495
0, Undefined
1, Monochrome area sensor
2, One-chip color area sensor
3, Two-chip color area sensor
4, Three-chip color area sensor
5, Color sequential area sensor
6, Monochrome linear sensor
7, Trilinear sensor
8, Color sequential linear sensor
# TODO(p): Add more TIFF/EP tags that can be only in IFD0.
37724, ImageSourceData # Adobe Photoshop TIFF Technical Notes
50706, DNGVersion # DNG 1.5.0.0
50707, DNGBackwardVersion # DNG 1.5.0.0
50708, UniqueCameraModel # DNG 1.5.0.0
50709, LocalizedCameraModel # DNG 1.5.0.0
# TODO(p): Add more DNG tags that can be only in IFD0.
# Exif 2.3 4.6.5
= Exif
33434, ExposureTime
33437, FNumber
34850, ExposureProgram
0, Not defined
1, Manual
2, Normal program
3, Aperture priority
4, Shutter priority
5, Creative program
6, Action program
7, Portrait mode
8, Landscape mode
34852, SpectralSensitivity
34855, PhotographicSensitivity
34856, OECF
34864, SensitivityType
0, Unknown
1, Standard output sensitivity
2, Recommended exposure index
3, ISO speed
4, SOS and REI
5, SOS and ISO speed
6, REI and ISO speed
7, SOS and REI and ISO speed
34865, StandardOutputSensitivity
34866, RecommendedExposureIndex
34867, ISOSpeed
34868, ISOSpeedLatitudeyyy
34869, ISOSpeedLatitudezzz
36864, ExifVersion
36867, DateTimeOriginal
36868, DateTimeDigitized
36880, OffsetTime # 2.31
36881, OffsetTimeOriginal # 2.31
36882, OffsetTimeDigitized # 2.31
37121, ComponentsConfiguration
0, Does not exist
1, Y
2, Cb
3, Cr
4, R
5, G
6, B
37122, CompressedBitsPerPixel
37377, ShutterSpeedValue
37378, ApertureValue
37379, BrightnessValue
37380, ExposureBiasValue
37381, MaxApertureValue
37382, SubjectDistance
37383, MeteringMode
0, Unknown
1, Average
2, CenterWeightedAverage
3, Spot
4, MultiSpot
5, Pattern
6, Partial
255, Other
37384, LightSource
0, Unknown
1, Daylight
2, Fluorescent
3, Tungsten (incandescent light)
4, Flash
9, Fine weather
10, Cloudy weather
11, Shade
12, Daylight fluorescent (D 5700 - 7100K)
13, Day white fluorescent (N 4600 - 5500K)
14, Cool white fluorescent (W 3800 - 4500K)
15, White fluorescent (WW 3250 - 3800K)
16, Warm white fluorescent (L 2600 - 3250K)
17, Standard light A
18, Standard light B
19, Standard light C
20, D55
21, D65
22, D75
23, D50
24, ISO studio tungsten
255, Other light source
37385, Flash
37386, FocalLength
37396, SubjectArea
37500, MakerNote
# TODO(p): Decode.
37510, UserComment
37520, SubSecTime
37521, SubSecTimeOriginal
37522, SubSecTimeDigitized
37888, Temperature # 2.31
37889, Humidity # 2.31
37890, Pressure # 2.31
37891, WaterDepth # 2.31
37892, Acceleration # 2.31
37893, CameraElevationAngle # 2.31
40960, FlashpixVersion
40961, ColorSpace
1, sRGB
65535, Uncalibrated
40962, PixelXDimension
40963, PixelYDimension
40964, RelatedSoundFile
40965, Interoperability IFD Pointer
41483, FlashEnergy
41484, SpatialFrequencyResponse
41486, FocalPlaneXResolution
41487, FocalPlaneYResolution
41488, FocalPlaneResolutionUnit
41492, SubjectLocation
41493, ExposureIndex
41495, SensingMethod
1, Not defined
2, One-chip color area sensor
3, Two-chip color area sensor
4, Three-chip color area sensor
5, Color sequential area sensor
7, Trilinear sensor
8, Color sequential linear sensor
41728, FileSource
0, Others
1, Scanner of transparent type
2, Scanner of reflex type
3, DSC
41729, SceneType
1, Directly-photographed image
41730, CFAPattern
41985, CustomRendered
0, Normal process
1, Custom process
41986, ExposureMode
0, Auto exposure
1, Manual exposure
2, Auto bracket
41987, WhiteBalance
0, Auto white balance
1, Manual white balance
41988, DigitalZoomRatio
41989, FocalLengthIn35mmFilm
41990, SceneCaptureType
0, Standard
1, Landscape
2, Portrait
3, Night scene
41991, GainControl
0, None
1, Low gain up
2, High gain up
3, Low gain down
4, High gain down
41992, Contrast
0, Normal
1, Soft
2, Hard
41993, Saturation
0, Normal
1, Low
2, High
41994, Sharpness
0, Normal
1, Soft
2, Hard
41995, DeviceSettingDescription
41996, SubjectDistanceRange
0, Unknown
1, Macro
2, Close view
3, Distant view
42016, ImageUniqueID
42032, CameraOwnerName
42033, BodySerialNumber
42034, LensSpecification
42035, LensMake
42036, LensModel
42037, LensSerialNumber
42080, CompositeImage # 2.32
42081, SourceImageNumberOfCompositeImage # 2.32
42082, SourceExposureTimesOfCompositeImage # 2.32
42240, Gamma
# Exif 2.3 4.6.6 (Notice it starts at 0.)
= Exif GPS
0, GPSVersionID
1, GPSLatitudeRef
2, GPSLatitude
3, GPSLongitudeRef
4, GPSLongitude
5, GPSAltitudeRef
0, Sea level
1, Sea level reference (negative value)
6, GPSAltitude
7, GPSTimeStamp
8, GPSSatellites
9, GPSStatus
10, GPSMeasureMode
11, GPSDOP
12, GPSSpeedRef
13, GPSSpeed
14, GPSTrackRef
15, GPSTrack
16, GPSImgDirectionRef
17, GPSImgDirection
18, GPSMapDatum
19, GPSDestLatitudeRef
20, GPSDestLatitude
21, GPSDestLongitudeRef
22, GPSDestLongitude
23, GPSDestBearingRef
24, GPSDestBearing
25, GPSDestDistanceRef
26, GPSDestDistance
27, GPSProcessingMethod
28, GPSAreaInformation
29, GPSDateStamp
30, GPSDifferential
0, Measurement without differential correction
1, Differential correction applied
31, GPSHPositioningError
# Exif 2.3 4.6.7 (Notice it starts at 1, and collides with GPS.)
= Exif Interoperability
1, InteroperabilityIndex
# CIPA DC-007-2021 5.2.3., 5.2.4. (But derive "field names" from "tag names".)
= MPF
45056, MP Format Version Number # MPFVersion
45057, Number of Images # NumberOfImages
45058, MP Entry # MPEntry
45059, Individual Image Unique ID List # ImageUIDList
45060, Total Number of Captured Frames # TotalFrames
45313, MP Individual Image Number # MPIndividualNum
45569, Panorama Scanning Orientation # PanOrientation
45570, Panorama Horizontal Overlap # PanOverlap_H
45571, Panorama Vertical Overlap # PanOverlap_V
45572, Base Viewpoint Number # BaseViewpointNum
45573, Convergence Angle # ConvergenceAngle
45574, Baseline Length # BaselineLength
45575, Divergence Angle # VerticalDivergence
45576, Horizontal Axis Distance # AxisDistance_X
45577, Vertical Axis Distance # AxisDistance_Y
45578, Collimation Axis Distance # AxisDistance_Z
45579, Yaw Angle # YawAngle
45580, Pitch Angle # PitchAngle
45581, Roll Angle # RollAngle

356
tiffer.h Normal file
View File

@@ -0,0 +1,356 @@
//
// tiffer.h: TIFF reading utilities
//
// Copyright (c) 2021 - 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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
// --- Utilities ---------------------------------------------------------------
static uint64_t
tiffer_u64be(const uint8_t *p)
{
return (uint64_t) p[0] << 56 | (uint64_t) p[1] << 48 |
(uint64_t) p[2] << 40 | (uint64_t) p[3] << 32 |
(uint64_t) p[4] << 24 | p[5] << 16 | p[6] << 8 | p[7];
}
static uint32_t
tiffer_u32be(const uint8_t *p)
{
return (uint32_t) p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
}
static uint16_t
tiffer_u16be(const uint8_t *p)
{
return (uint16_t) p[0] << 8 | p[1];
}
static uint64_t
tiffer_u64le(const uint8_t *p)
{
return (uint64_t) p[7] << 56 | (uint64_t) p[6] << 48 |
(uint64_t) p[5] << 40 | (uint64_t) p[4] << 32 |
(uint64_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
}
static uint32_t
tiffer_u32le(const uint8_t *p)
{
return (uint32_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
}
static uint16_t
tiffer_u16le(const uint8_t *p)
{
return (uint16_t) p[1] << 8 | p[0];
}
// --- TIFF --------------------------------------------------------------------
// libtiff is a mess, and the format is not particularly complicated.
// Exiv2 is senselessly copylefted, and cannot do much.
// libexif is only marginally better.
// ExifTool is too user-oriented.
struct un {
uint64_t (*u64) (const uint8_t *);
uint32_t (*u32) (const uint8_t *);
uint16_t (*u16) (const uint8_t *);
};
static struct un tiffer_unbe = {tiffer_u64be, tiffer_u32be, tiffer_u16be};
static struct un tiffer_unle = {tiffer_u64le, tiffer_u32le, tiffer_u16le};
struct tiffer {
struct un *un;
const uint8_t *begin, *p, *end;
uint16_t remaining_fields;
};
static bool
tiffer_u32(struct tiffer *self, uint32_t *u)
{
if (self->end - self->p < 4)
return false;
*u = self->un->u32(self->p);
self->p += 4;
return true;
}
static bool
tiffer_u16(struct tiffer *self, uint16_t *u)
{
if (self->end - self->p < 2)
return false;
*u = self->un->u16(self->p);
self->p += 2;
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
tiffer_init(struct tiffer *self, const uint8_t *tiff, size_t len)
{
self->un = NULL;
self->begin = self->p = tiff;
self->end = tiff + len;
self->remaining_fields = 0;
const uint8_t
le[4] = {'I', 'I', 42, 0},
be[4] = {'M', 'M', 0, 42};
if (tiff + 8 > self->end)
return false;
else if (!memcmp(tiff, le, sizeof le))
self->un = &tiffer_unle;
else if (!memcmp(tiff, be, sizeof be))
self->un = &tiffer_unbe;
else
return false;
self->p = tiff + 4;
// The first IFD needs to be read by caller explicitly,
// even though it's required to be present by TIFF 6.0.
return true;
}
/// Read the next IFD in a sequence.
static bool
tiffer_next_ifd(struct tiffer *self)
{
// All fields from any previous IFD need to be read first.
if (self->remaining_fields)
return false;
uint32_t ifd_offset = 0;
if (!tiffer_u32(self, &ifd_offset))
return false;
// There is nothing more to read, this chain has terminated.
if (!ifd_offset)
return false;
// Note that TIFF 6.0 requires there to be at least one entry,
// but there is no need for us to check it.
self->p = self->begin + ifd_offset;
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 (tiffer_length(self) < offset)
return false;
*subreader = *self;
subreader->p = subreader->begin + offset;
return tiffer_u16(subreader, &subreader->remaining_fields);
}
enum tiffer_type {
TIFFER_BYTE = 1, TIFFER_ASCII, TIFFER_SHORT, TIFFER_LONG,
TIFFER_RATIONAL,
TIFFER_SBYTE, TIFFER_UNDEFINED, TIFFER_SSHORT, TIFFER_SLONG,
TIFFER_SRATIONAL,
TIFFER_FLOAT,
TIFFER_DOUBLE,
// This last type from TIFF Technical Note 1 isn't really used much.
TIFFER_IFD,
};
static size_t
tiffer_value_size(enum tiffer_type type)
{
switch (type) {
case TIFFER_BYTE:
case TIFFER_SBYTE:
case TIFFER_ASCII:
case TIFFER_UNDEFINED:
return 1;
case TIFFER_SHORT:
case TIFFER_SSHORT:
return 2;
case TIFFER_LONG:
case TIFFER_SLONG:
case TIFFER_FLOAT:
case TIFFER_IFD:
return 4;
case TIFFER_RATIONAL:
case TIFFER_SRATIONAL:
case TIFFER_DOUBLE:
return 8;
default:
return 0;
}
}
/// A lean iterator for values within entries.
struct tiffer_entry {
uint16_t tag;
enum tiffer_type type;
// For {S,}BYTE, ASCII, UNDEFINED, use these fields directly.
const uint8_t *p;
uint32_t remaining_count;
};
static bool
tiffer_next_value(struct tiffer_entry *entry)
{
if (!entry->remaining_count)
return false;
entry->p += tiffer_value_size(entry->type);
entry->remaining_count--;
return true;
}
static bool
tiffer_integer(
const struct tiffer *self, const struct tiffer_entry *entry, int64_t *out)
{
if (!entry->remaining_count)
return false;
// Somewhat excessively lenient, intended for display.
// TIFF 6.0 only directly suggests that a reader is should accept
// any of BYTE/SHORT/LONG for unsigned integers.
switch (entry->type) {
case TIFFER_BYTE:
case TIFFER_ASCII:
case TIFFER_UNDEFINED:
*out = *entry->p;
return true;
case TIFFER_SBYTE:
*out = (int8_t) *entry->p;
return true;
case TIFFER_SHORT:
*out = self->un->u16(entry->p);
return true;
case TIFFER_SSHORT:
*out = (int16_t) self->un->u16(entry->p);
return true;
case TIFFER_LONG:
case TIFFER_IFD:
*out = self->un->u32(entry->p);
return true;
case TIFFER_SLONG:
*out = (int32_t) self->un->u32(entry->p);
return true;
default:
return false;
}
}
static bool
tiffer_rational(const struct tiffer *self, const struct tiffer_entry *entry,
int64_t *numerator, int64_t *denominator)
{
if (!entry->remaining_count)
return false;
// Somewhat excessively lenient, intended for display.
switch (entry->type) {
case TIFFER_RATIONAL:
*numerator = self->un->u32(entry->p);
*denominator = self->un->u32(entry->p + 4);
return true;
case TIFFER_SRATIONAL:
*numerator = (int32_t) self->un->u32(entry->p);
*denominator = (int32_t) self->un->u32(entry->p + 4);
return true;
default:
if (tiffer_integer(self, entry, numerator)) {
*denominator = 1;
return true;
}
return false;
}
}
static bool
tiffer_real(
const struct tiffer *self, const struct tiffer_entry *entry, double *out)
{
if (!entry->remaining_count)
return false;
// Somewhat excessively lenient, intended for display.
// Assuming the host architecture uses IEEE 754.
switch (entry->type) {
int64_t numerator, denominator;
case TIFFER_FLOAT:
*out = *(float *) entry->p;
return true;
case TIFFER_DOUBLE:
*out = *(double *) entry->p;
return true;
default:
if (tiffer_rational(self, entry, &numerator, &denominator)) {
*out = (double) numerator / denominator;
return true;
}
return false;
}
}
static bool
tiffer_next_entry(struct tiffer *self, struct tiffer_entry *entry)
{
if (!self->remaining_fields)
return false;
uint16_t type = entry->type = 0xFFFF;
if (!tiffer_u16(self, &entry->tag) || !tiffer_u16(self, &type) ||
!tiffer_u32(self, &entry->remaining_count))
return false;
// Short values may and will be inlined, rather than pointed to.
size_t values_size = tiffer_value_size(type) * entry->remaining_count;
uint32_t offset = 0;
if (values_size <= sizeof offset) {
entry->p = self->p;
self->p += sizeof offset;
} else if (tiffer_u32(self, &offset) && tiffer_length(self) >= offset) {
entry->p = self->begin + offset;
} else {
return false;
}
// All entries are pre-checked not to overflow.
if (values_size > PTRDIFF_MAX ||
self->end - entry->p < (ptrdiff_t) values_size)
return false;
// Setting it at the end may provide an indication while debugging.
entry->type = type;
self->remaining_fields--;
return true;
}

1
tools/.gitignore vendored
View File

@@ -1 +0,0 @@
/pnginfo

View File

@@ -1,13 +0,0 @@
SHELL = /bin/sh
# libjq 1.6 lacks a pkg-config file, and there is no release in sight.
CFLAGS = -g -O2 -Wall -Wextra `pkg-config --cflags $(deps)`
LDFLAGS = -ljq `pkg-config --libs $(deps)`
deps = libpng
targets = pnginfo
all: $(targets)
clean:
rm -f $(targets)
.PHONY: all clean

View File

@@ -1,7 +1,7 @@
//
// fastiv-io-benchmark.c: see if we're worth the name
// benchmark-io.c: measure and compare image loading times
//
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 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.
@@ -15,10 +15,11 @@
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include <time.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gdk/gdk.h>
#include <time.h>
#include "fastiv-view.h"
#include "fiv-io.h"
static double
timestamp(void)
@@ -31,26 +32,36 @@ timestamp(void)
static void
one_file(const char *filename)
{
double since_us = timestamp();
cairo_surface_t *loaded_by_us = fastiv_io_open(filename, NULL);
GFile *file = g_file_new_for_commandline_arg(filename);
double since_us = timestamp(), us = 0;
FivIoOpenContext ctx = {
.uri = g_file_get_uri(file),
.screen_dpi = 96,
// Only using this array as a redirect.
.warnings = g_ptr_array_new_with_free_func(g_free),
};
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);
double us = timestamp() - since_us;
fiv_io_image_unref(loaded_by_us);
us = timestamp() - since_us;
double since_pixbuf = timestamp();
double since_pixbuf = timestamp(), pixbuf = 0;
GdkPixbuf *gdk_pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
if (!gdk_pixbuf)
return;
if (gdk_pixbuf) {
cairo_surface_t *loaded_by_pixbuf =
gdk_cairo_surface_create_from_pixbuf(gdk_pixbuf, 1, NULL);
g_object_unref(gdk_pixbuf);
cairo_surface_destroy(loaded_by_pixbuf);
pixbuf = timestamp() - since_pixbuf;
}
cairo_surface_t *loaded_by_pixbuf =
gdk_cairo_surface_create_from_pixbuf(gdk_pixbuf, 1, NULL);
g_object_unref(gdk_pixbuf);
cairo_surface_destroy(loaded_by_pixbuf);
double pixbuf = timestamp() - since_pixbuf;
printf("%f\t%f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename);
printf("%.3f\t%.3f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename);
}
int

View File

@@ -1,6 +1,6 @@
#!/bin/sh -e
# Remove thumbnails with URIs pointing to at this moment non-existing files.
make pnginfo
ninja pnginfo
pnginfo=$(pwd)/pnginfo cache_home=${XDG_CACHE_HOME:-$HOME/.cache}
for size in normal large x-large xx-large; do

210
tools/hotpixels.c Normal file
View File

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

286
tools/info.c Normal file
View File

@@ -0,0 +1,286 @@
//
// info.c: acquire information about JPEG/TIFF/BMFF/WebP files in JSON format
//
// Copyright (c) 2021 - 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 "info.h"
#include <jv.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- ISO/IEC base media file format ------------------------------------------
// ISO/IEC 14496-12:2015(E), used to be publicly available, now there's only:
// https://mpeg.chiariglione.org/standards/mpeg-4/iso-base-media-file-format/text-isoiec-14496-12-5th-edition
// but people have managed to archive the final version as well:
// https://b.goeswhere.com/ISO_IEC_14496-12_2015.pdf
//
// ISO/IEC 23008-12:2017 Information technology -
// High efficiency coding and media delivery in heterogeneous environments -
// Part 12: Image File Format + Cor 1:2020 Technical Corrigendum 1
// https://standards.iso.org/ittf/PubliclyAvailableStandards/
static jv
parse_bmff_box(jv o, const char *type, const uint8_t *data, size_t len)
{
// TODO(p): Parse out "uuid"'s uint8_t[16] initial field, present as hex.
// TODO(p): Parse out "ftyp" contents: 14496-12:2015 4.3
// TODO(p): Parse out other important boxes: 14496-12:2015 8+
return add_to_subarray(o, "boxes", jv_string(type));
}
static bool
detect_bmff(const uint8_t *p, size_t len)
{
// 4.2 Object Structure--this box need not be present, nor at the beginning
// TODO(p): What does `aligned(8)` mean? It's probably in bits.
return len >= 8 && !memcmp(p + 4, "ftyp", 4);
}
static jv
parse_bmff(jv o, const uint8_t *p, size_t len)
{
if (!detect_bmff(p, len))
return add_error(o, "not BMFF at all or unsupported");
const uint8_t *end = p + len;
while (p < end) {
if (end - p < 8) {
o = add_warning(o, "box framing mismatch");
break;
}
char type[5] = "";
memcpy(type, p + 4, 4);
uint64_t box_size = u32be(p);
const uint8_t *data = p + 8;
if (box_size == 1) {
if (end - p < 16) {
o = add_warning(o, "unexpected EOF");
break;
}
box_size = u64be(data);
data += 8;
} else if (!box_size)
box_size = end - p;
if (box_size > (uint64_t) (end - p)) {
o = add_warning(o, "unexpected EOF");
break;
}
size_t data_len = box_size - (data - p);
o = parse_bmff_box(o, type, data, data_len);
p += box_size;
}
return o;
}
// --- WebP --------------------------------------------------------------------
// libwebp won't let us simply iterate over all chunks, so handroll it.
//
// https://github.com/webmproject/libwebp/blob/master/doc/webp-container-spec.txt
// https://github.com/webmproject/libwebp/blob/master/doc/webp-lossless-bitstream-spec.txt
// https://datatracker.ietf.org/doc/html/rfc6386
//
// Pretty versions, hopefully not outdated:
// https://developers.google.com/speed/webp/docs/riff_container
// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
static bool
detect_webp(const uint8_t *p, size_t len)
{
return len >= 12 && !memcmp(p, "RIFF", 4) && !memcmp(p + 8, "WEBP", 4);
}
static jv
parse_webp_vp8(jv o, const uint8_t *p, size_t len)
{
if (len < 10 || (p[0] & 1) != 0 /* key frame */ ||
p[3] != 0x9d || p[4] != 0x01 || p[5] != 0x2a) {
return add_warning(o, "invalid VP8 chunk");
}
o = jv_set(o, jv_string("width"), jv_number(u16le(p + 6) & 0x3fff));
o = jv_set(o, jv_string("height"), jv_number(u16le(p + 8) & 0x3fff));
return o;
}
static jv
parse_webp_vp8l(jv o, const uint8_t *p, size_t len)
{
if (len < 5 || p[0] != 0x2f)
return add_warning(o, "invalid VP8L chunk");
// Reading LSB-first from a little endian value means reading in order.
uint32_t header = u32le(p + 1);
o = jv_set(o, jv_string("width"), jv_number((header & 0x3fff) + 1));
header >>= 14;
o = jv_set(o, jv_string("height"), jv_number((header & 0x3fff) + 1));
header >>= 14;
o = jv_set(o, jv_string("alpha_is_used"), jv_bool(header & 1));
return o;
}
static jv
parse_webp_vp8x(jv o, const uint8_t *p, size_t len)
{
if (len < 10)
return add_warning(o, "invalid VP8X chunk");
// Most of the fields in this chunk are duplicate or inferrable.
// Probably not worth decoding or verifying.
// TODO(p): For animations, we need to use the width and height from here.
uint8_t flags = p[0];
o = jv_set(o, jv_string("animation"), jv_bool((flags >> 1) & 1));
return o;
}
static jv
parse_webp(jv o, const uint8_t *p, size_t len)
{
if (!detect_webp(p, len))
return add_error(o, "not a WEBP file");
// TODO(p): This can still be parseable.
// TODO(p): Warn on trailing data.
uint32_t size = u32le(p + 4);
if (8 + size < len)
return add_error(o, "truncated file");
const uint8_t *end = p + 8 + size;
p += 12;
jv chunks = jv_array();
while (p < end) {
if (end - p < 8) {
o = add_warning(o, "framing mismatch");
printf("%ld", end - p);
break;
}
uint32_t chunk_size = u32le(p + 4);
uint32_t chunk_advance = (chunk_size + 1) & ~1;
if (p + 8 + chunk_advance > end) {
o = add_warning(o, "runaway chunk payload");
break;
}
char fourcc[5] = "";
memcpy(fourcc, p, 4);
chunks = jv_array_append(chunks, jv_string(fourcc));
p += 8;
// TODO(p): Decode more chunks.
if (!strcmp(fourcc, "VP8 "))
o = parse_webp_vp8(o, p, chunk_size);
if (!strcmp(fourcc, "VP8L"))
o = parse_webp_vp8l(o, p, chunk_size);
if (!strcmp(fourcc, "VP8X"))
o = parse_webp_vp8x(o, p, chunk_size);
if (!strcmp(fourcc, "EXIF"))
o = parse_exif(o, p, chunk_size);
if (!strcmp(fourcc, "ICCP"))
o = parse_icc(o, p, chunk_size);
p += chunk_advance;
}
return jv_set(o, jv_string("chunks"), chunks);
}
// --- I/O ---------------------------------------------------------------------
static struct {
const char *name;
bool (*detect) (const uint8_t *, size_t);
jv (*parse) (jv, const uint8_t *, size_t);
} formats[] = {
{"JPEG", detect_jpeg, parse_jpeg},
{"TIFF", detect_tiff, parse_tiff},
{"BMFF", detect_bmff, parse_bmff},
{"WebP", detect_webp, parse_webp},
};
static jv
parse_any(jv o, const uint8_t *p, size_t len)
{
// TODO(p): Also see if the file extension is appropriate.
for (size_t i = 0; i < sizeof formats / sizeof *formats; i++) {
if (!formats[i].detect(p, len))
continue;
if (getenv("INFO_IDENTIFY"))
o = jv_set(o, jv_string("format"), jv_string(formats[i].name));
return formats[i].parse(o, p, len);
}
return add_error(o, "unsupported file format");
}
static jv
do_file(const char *filename, jv o)
{
const char *err = NULL;
FILE *fp = fopen(filename, "rb");
if (!fp) {
err = strerror(errno);
goto error;
}
uint8_t *data = NULL, buf[256 << 10];
size_t n, len = 0;
while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
data = realloc(data, len + n);
memcpy(data + len, buf, n);
len += n;
}
if (ferror(fp)) {
err = strerror(errno);
goto error_read;
}
#if 0
// Not sure if I want to ensure their existence...
o = jv_object_set(o, jv_string("info"), jv_array());
o = jv_object_set(o, jv_string("warnings"), jv_array());
#endif
o = parse_any(o, data, len);
error_read:
fclose(fp);
free(data);
error:
if (err)
o = add_error(o, err);
return o;
}
int
main(int argc, char *argv[])
{
// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
// Usage: find . -print0 | xargs -0 ./info
for (int i = 1; i < argc; i++) {
const char *filename = argv[i];
jv o = jv_object();
o = jv_object_set(o, jv_string("filename"), jv_string(filename));
o = do_file(filename, o);
jv_dumpf(o, stdout, 0 /* JV_PRINT_SORTED would discard information. */);
fputc('\n', stdout);
}
return 0;
}

1202
tools/info.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,12 @@
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include "info.h"
#include <png.h>
#include <jv.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
@@ -63,28 +66,120 @@ strfmt(const char *format, ...)
return result;
}
static uint8_t *
hexbin(const char *string, size_t *len)
{
static const char *alphabet = "0123456789abcdef";
uint8_t *buf = calloc(1, strlen(string) / 2 + 1), *p = buf;
while (true) {
while (*string && strchr(" \t\n\r\v\f", *string))
string++;
if (!*string)
break;
const char *hi, *lo;
if (!(hi = strchr(alphabet, tolower(*string++))) || !*string ||
!(lo = strchr(alphabet, tolower(*string++)))) {
free(buf);
return NULL;
}
*p++ = (hi - alphabet) << 4 | (lo - alphabet);
}
*len = p - buf;
return buf;
}
// --- Analysis ----------------------------------------------------------------
static void
redirect_libpng_error(png_structp pngp, const char* message)
static uint8_t *
extract_imagemagick_attribute(const char *string, size_t *len)
{
char **storage = png_get_error_ptr(pngp);
*storage = strfmt("%s", message);
if (*string++ != '\n')
return NULL;
// TODO(p): Try to verify this profile type, also present in the key,
// though beware that it may contain "generic profile" for APP1, etc.
const char *type = string;
if (!(string = strchr(type, '\n')))
return NULL;
// strtol() skips initial whitespace, this is mostly desired.
char *end = NULL;
long size = strtol(++string, &end, 10);
if (size < 0 || end == string || *end++ != '\n')
return NULL;
uint8_t *bin = hexbin(end, len);
if (!bin || (long) *len != size) {
free(bin);
return NULL;
}
return bin;
}
static jv
retrieve_texts(png_structp pngp, png_infop infop)
extract_imagemagick_exif(jv o, const char *string)
{
size_t exif_len = 0;
uint8_t *exif = extract_imagemagick_attribute(string, &exif_len);
if (!exif)
return add_warning(o, "invalid ImageMagick 'exif'");
o = parse_exif(o, exif, exif_len);
free(exif);
return o;
}
static jv
extract_imagemagick_psir(jv o, const char *string)
{
size_t psir_len = 0;
uint8_t *psir = extract_imagemagick_attribute(string, &psir_len);
if (!psir)
return add_warning(o, "invalid ImageMagick '8bim'");
o = parse_psir(o, psir, psir_len);
free(psir);
return o;
}
static bool
process_text(jv *o, png_textp text)
{
// TODO(p): Refactor info.h, so that it's the value of the text chunk,
// and that warnings are added to the top-level JSON.
// These seem to originate in ImageMagick,
// but are also used by ExifTool and GIMP, among others.
// https://exiftool.org/TagNames/PNG.html
// TODO(p): "iptc": may contain 8BIM or IPTC IIM directly.
// TODO(p): "APP1": may contain Exif or XMP.
if (!strcmp(text->key, "Raw profile type exif")) {
*o = extract_imagemagick_exif(*o, text->text);
return true;
}
if (!strcmp(text->key, "Raw profile type 8bim")) {
*o = extract_imagemagick_psir(*o, text->text);
return true;
}
return false;
}
static jv
retrieve_texts(jv o, png_structp pngp, png_infop infop)
{
int texts_len = 0;
png_textp texts = NULL;
png_get_text(pngp, infop, &texts, &texts_len);
jv o = jv_object();
jv to = jv_object();
for (int i = 0; i < texts_len; i++) {
png_textp text = texts + i;
o = jv_object_set(o, jv_string(text->key), jv_string(text->text));
to = jv_object_set(to, jv_string(text->key),
process_text(&o, text) ? jv_true() : jv_string(text->text));
}
return o;
return jv_object_set(o, jv_string("texts"), to);
}
static jv
@@ -146,18 +241,34 @@ extract_chunks(png_structp pngp, png_infop infop)
jv set = jv_object();
png_unknown_chunkp unknowns = NULL;
int unknowns_len = png_get_unknown_chunks(pngp, infop, &unknowns);
for (int i = 0; i < unknowns_len; i++)
for (int i = 0; i < unknowns_len; i++) {
set = jv_object_set(set,
jv_string((const char *) unknowns[i].name), jv_true());
// https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html
//
// Some software also supports the adjacent zXIf proposal,
// which ended up being rejected. Such files are rare, and best ignored.
// http://www.simplesystems.org/png-group/proposals/zXIf/history
// /png-proposed-zXIf-chunk-2017-03-05.html
if (!strcmp((const char *) unknowns[i].name, "eXIf"))
o = parse_exif(o, unknowns[i].data, unknowns[i].size);
}
jv a = jv_array();
jv_object_keys_foreach(set, key)
a = jv_array_append(a, jv_copy(key));
o = jv_object_set(o, jv_string("chunks"), a);
jv_free(set);
o = jv_object_set(o, jv_string("texts"), retrieve_texts(pngp, infop));
return o;
return retrieve_texts(o, pngp, infop);
}
static void
redirect_libpng_error(png_structp pngp, const char* message)
{
char **storage = png_get_error_ptr(pngp);
*storage = strfmt("%s", message);
}
static jv
@@ -173,6 +284,7 @@ do_file(const char *filename, volatile jv o)
goto error;
}
// TODO(p): Extract libpng warnings.
png_structp pngp = png_create_read_struct(PNG_LIBPNG_VER_STRING,
(png_voidp) &err, redirect_libpng_error, NULL);
if (!pngp) {
@@ -227,7 +339,7 @@ error_png:
fclose(fp);
error:
if (err) {
o = jv_object_set(o, jv_string("error"), jv_string(err));
o = add_error(o, err);
free(err);
}
return o;
@@ -236,6 +348,8 @@ error:
int
main(int argc, char *argv[])
{
(void) parse_icc;
// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
// Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
for (int i = 1; i < argc; i++) {

175
tools/rawinfo.c Normal file
View File

@@ -0,0 +1,175 @@
//
// rawinfo.c: acquire information about raw image files in JSON format
//
// 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 "info.h"
#include <jv.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- Raw image files ---------------------------------------------------------
// This is in principle similar to LibRaw's `raw-identify -v`,
// but the output is machine-processable.
static jv
parse_raw(jv o, const uint8_t *p, size_t len)
{
libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
if (!iprc)
return add_error(o, "failed to obtain a LibRaw handle");
int err = 0;
if ((err = libraw_open_buffer(iprc, p, len))) {
libraw_close(iprc);
return add_error(o, libraw_strerror(err));
}
// -> iprc->rawparams.shot_select
o = jv_set(o, jv_string("count"), jv_number(iprc->idata.raw_count));
o = jv_set(o, jv_string("width"), jv_number(iprc->sizes.width));
o = jv_set(o, jv_string("height"), jv_number(iprc->sizes.height));
o = jv_set(o, jv_string("flip"), jv_number(iprc->sizes.flip));
o = jv_set(o, jv_string("pixel_aspect_ratio"),
jv_number(iprc->sizes.pixel_aspect));
if ((err = libraw_adjust_sizes_info_only(iprc))) {
o = add_warning(o, libraw_strerror(err));
} else {
o = jv_set(
o, jv_string("output_width"), jv_number(iprc->sizes.iwidth));
o = jv_set(
o, jv_string("output_height"), jv_number(iprc->sizes.iheight));
}
jv thumbnails = jv_array();
for (int i = 0; i < iprc->thumbs_list.thumbcount; i++) {
libraw_thumbnail_item_t *item = iprc->thumbs_list.thumblist + i;
const char *format = "?";
switch (item->tformat) {
case LIBRAW_INTERNAL_THUMBNAIL_UNKNOWN:
format = "unknown";
break;
case LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB:
format = "Kodak thumbnail";
break;
case LIBRAW_INTERNAL_THUMBNAIL_KODAK_YCBCR:
format = "Kodak YCbCr";
break;
case LIBRAW_INTERNAL_THUMBNAIL_KODAK_RGB:
format = "Kodak RGB";
break;
case LIBRAW_INTERNAL_THUMBNAIL_JPEG:
format = "JPEG";
break;
case LIBRAW_INTERNAL_THUMBNAIL_LAYER:
format = "layer";
break;
case LIBRAW_INTERNAL_THUMBNAIL_ROLLEI:
format = "Rollei";
break;
case LIBRAW_INTERNAL_THUMBNAIL_PPM:
format = "PPM";
break;
case LIBRAW_INTERNAL_THUMBNAIL_PPM16:
format = "PPM16";
break;
case LIBRAW_INTERNAL_THUMBNAIL_X3F:
format = "X3F";
break;
}
jv to = JV_OBJECT(
jv_string("width"), jv_number(item->twidth),
jv_string("height"), jv_number(item->theight),
jv_string("flip"), jv_number(item->tflip),
jv_string("format"), jv_string(format));
if (item->tformat == LIBRAW_INTERNAL_THUMBNAIL_JPEG &&
item->toffset > 0 &&
(size_t) item->toffset + item->tlength <= len) {
to = jv_set(to, jv_string("JPEG"),
parse_jpeg(jv_object(), p + item->toffset, item->tlength));
}
thumbnails = jv_array_append(thumbnails, to);
}
libraw_close(iprc);
return jv_set(o, jv_string("thumbnails"), thumbnails);
}
// --- I/O ---------------------------------------------------------------------
static jv
do_file(const char *filename, jv o)
{
const char *err = NULL;
FILE *fp = fopen(filename, "rb");
if (!fp) {
err = strerror(errno);
goto error;
}
uint8_t *data = NULL, buf[256 << 10];
size_t n, len = 0;
while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
data = realloc(data, len + n);
memcpy(data + len, buf, n);
len += n;
}
if (ferror(fp)) {
err = strerror(errno);
goto error_read;
}
o = parse_raw(o, data, len);
error_read:
fclose(fp);
free(data);
error:
if (err)
o = add_error(o, err);
return o;
}
int
main(int argc, char *argv[])
{
// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
// Usage: find . -print0 | xargs -0 ./rawinfo
for (int i = 1; i < argc; i++) {
const char *filename = argv[i];
jv o = jv_object();
o = jv_object_set(o, jv_string("filename"), jv_string(filename));
o = do_file(filename, o);
jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
fputc('\n', stdout);
}
return 0;
}

201
xdg.c Normal file
View File

@@ -0,0 +1,201 @@
//
// xdg.c: various *nix desktop utilities
//
// Copyright (c) 2021, 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 <glib.h>
#include <stdlib.h>
#include <string.h>
/// Add `element` to the `output` set. `relation` is a map of sets of strings
/// defining is-a relations, and is traversed recursively.
static void
add_applying_transitive_closure(
const char *element, GHashTable *relation, GHashTable *output)
{
// Stop condition.
if (!g_hash_table_add(output, g_strdup(element)))
return;
// TODO(p): Iterate over all aliases of `element` in addition to
// any direct match (and rename this no-longer-generic function).
GHashTable *targets = g_hash_table_lookup(relation, element);
if (!targets)
return;
GHashTableIter iter;
g_hash_table_iter_init(&iter, targets);
gpointer key = NULL, value = NULL;
while (g_hash_table_iter_next(&iter, &key, &value))
add_applying_transitive_closure(key, relation, output);
}
char *
get_xdg_home_dir(const char *var, const char *default_)
{
const char *env = getenv(var);
if (env && g_path_is_absolute(env))
return g_strdup(env);
#ifdef G_OS_WIN32
return g_build_filename(g_get_home_dir(), default_, NULL);
#else
// The specification doesn't handle a missing HOME variable explicitly.
// Implicitly, assuming Bourne shell semantics, it simply resolves empty.
const char *home = getenv("HOME");
return g_build_filename(home ? home : "", default_, NULL);
#endif
}
// Reïmplemented partly due to https://gitlab.gnome.org/GNOME/glib/-/issues/2501
static gchar **
get_xdg_data_dirs(void)
{
// GStrvBuilder is too new, it would help a little bit.
GPtrArray *output = g_ptr_array_new_with_free_func(g_free);
#ifdef G_OS_WIN32
g_ptr_array_add(output, g_strdup(g_get_user_data_dir()));
for (const gchar *const *p = g_get_system_data_dirs(); *p; p++)
g_ptr_array_add(output, g_strdup(*p));
#else
g_ptr_array_add(output, get_xdg_home_dir("XDG_DATA_HOME", ".local/share"));
const char *xdg_data_dirs = "";
if (!(xdg_data_dirs = getenv("XDG_DATA_DIRS")) || !*xdg_data_dirs)
xdg_data_dirs = "/usr/local/share/:/usr/share/";
gchar **candidates = g_strsplit(xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, 0);
for (gchar **p = candidates; *p; p++) {
if (g_path_is_absolute(*p))
g_ptr_array_add(output, *p);
else
g_free(*p);
}
g_free(candidates);
#endif
g_ptr_array_add(output, NULL);
return (gchar **) g_ptr_array_free(output, FALSE);
}
// --- Filtering ---------------------------------------------------------------
// Derived from shared-mime-info-spec 0.21.
static void
read_mime_subclasses(const char *path, GHashTable *subclass_sets)
{
gchar *data = NULL;
if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */))
return;
// The format of this file is unspecified,
// but in practice it's a list of space-separated media types.
gchar *datasave = NULL;
for (gchar *line = strtok_r(data, "\r\n", &datasave); line;
line = strtok_r(NULL, "\r\n", &datasave)) {
gchar *linesave = NULL,
*subclass = strtok_r(line, " ", &linesave),
*superclass = strtok_r(NULL, " ", &linesave);
// Nothing about comments is specified, we're being nice.
if (!subclass || *subclass == '#' || !superclass)
continue;
GHashTable *set = NULL;
if (!(set = g_hash_table_lookup(subclass_sets, superclass))) {
set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
g_hash_table_insert(subclass_sets, g_strdup(superclass), set);
}
g_hash_table_add(set, g_strdup(subclass));
}
g_free(data);
}
static gboolean
filter_mime_globs(const char *path, guint is_globs2, GHashTable *supported_set,
GHashTable *output_set)
{
gchar *data = NULL;
if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */))
return FALSE;
gchar *datasave = NULL;
for (const char *line = strtok_r(data, "\r\n", &datasave); line;
line = strtok_r(NULL, "\r\n", &datasave)) {
if (*line == '#')
continue;
// We do not support __NOGLOBS__, nor even parse out the "cs" flag.
// The weight is irrelevant.
gchar **f = g_strsplit(line, ":", 0);
if (g_strv_length(f) >= is_globs2 + 2) {
const gchar *type = f[is_globs2 + 0], *glob = f[is_globs2 + 1];
if (g_hash_table_contains(supported_set, type))
g_hash_table_add(output_set, g_utf8_strdown(glob, -1));
}
g_strfreev(f);
}
g_free(data);
return TRUE;
}
char **
extract_mime_globs(const char **media_types)
{
gchar **data_dirs = get_xdg_data_dirs();
// The mime.cache format is inconvenient to parse,
// we'll do it from the text files manually, and once only.
GHashTable *subclass_sets = g_hash_table_new_full(
g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_hash_table_destroy);
for (gsize i = 0; data_dirs[i]; i++) {
gchar *path =
g_build_filename(data_dirs[i], "mime", "subclasses", NULL);
read_mime_subclasses(path, subclass_sets);
g_free(path);
}
// A hash set of all supported media types, including subclasses,
// but not aliases.
GHashTable *supported =
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
while (*media_types) {
add_applying_transitive_closure(
*media_types++, subclass_sets, supported);
}
g_hash_table_destroy(subclass_sets);
// We do not support the distinction of case-sensitive globs (:cs).
GHashTable *globs =
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
for (gsize i = 0; data_dirs[i]; i++) {
gchar *path2 = g_build_filename(data_dirs[i], "mime", "globs2", NULL);
gchar *path1 = g_build_filename(data_dirs[i], "mime", "globs", NULL);
if (!filter_mime_globs(path2, TRUE, supported, globs))
filter_mime_globs(path1, FALSE, supported, globs);
g_free(path2);
g_free(path1);
}
g_strfreev(data_dirs);
g_hash_table_destroy(supported);
gchar **result = (gchar **) g_hash_table_get_keys_as_array(globs, NULL);
g_hash_table_steal_all(globs);
g_hash_table_destroy(globs);
return result;
}

View File

@@ -1,5 +1,5 @@
//
// fastiv-browser.h: fast image viewer - filesystem browser widget
// xdg.h: various *nix desktop utilities
//
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
//
@@ -15,11 +15,5 @@
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#pragma once
#include <gtk/gtk.h>
#define FASTIV_TYPE_BROWSER (fastiv_browser_get_type())
G_DECLARE_FINAL_TYPE(FastivBrowser, fastiv_browser, FASTIV, BROWSER, GtkWidget)
void fastiv_browser_load(FastivBrowser *self, const char *path);
char *get_xdg_home_dir(const char *var, const char *default_);
char **extract_mime_globs(const char **media_types);