Compare commits

...

150 Commits

Author SHA1 Message Date
2234fd008d MSYS2: add a comment about realpath
All checks were successful
Alpine 3.21 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-03 18:05:41 +01:00
0fceaf7728 Bump Wuffs
All checks were successful
Alpine 3.21 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-02 02:09:28 +01:00
c46fc73c34 Prefill the 'Enter location' dialog 2025-11-02 02:09:28 +01:00
bdd18fc898 Very slightly improve file updates on macOS
Some checks failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
openSUSE 15.5 Success
OpenBSD 7.7 Success
OpenBSD 7.6 Unsupported
Alpine 3.21 Success
2025-10-18 17:47:25 +01:00
cf6ded1d03 Make browser Cmd+click open new windows on macOS
Some checks failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.6 Scripts failed
openSUSE 15.5 Success
Alpine 3.21 Success
2025-10-18 15:24:36 +01:00
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
52 changed files with 6854 additions and 3978 deletions

4
.gitmodules vendored
View File

@@ -1,6 +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 = liberty
path = submodules/liberty
url = https://git.janouch.name/p/liberty.git

View File

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

View File

@@ -2,7 +2,7 @@ fiv
===
'fiv' is a slightly unconventional, general-purpose image browser and viewer
for Linux (that said, macOS and Windows ports are possible).
for Linux and Windows (macOS still has major issues).
image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
@@ -13,7 +13,7 @@ Features
photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
modules manage to load.
- Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
- Makes use of 30-bit X.org visuals, whenever it's possible and appropriate.
- Can make use of 30-bit X.org visuals, under certain conditions.
- Has a notion of pages, and tries to load all included content within files.
- Can keep the zoom and position when browsing, to help with comparing
zoomed-in images.
@@ -33,45 +33,63 @@ Not necessarily in this order.
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR.
Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version using Arch Linux's
https://aur.archlinux.org/packages/fiv-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
https://janouch.name/cd[Windows installers can be found here],
you want the _x86_64_ version.
Building and Running
--------------------
Build-only dependencies:
Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
Runtime dependencies:
gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info, libturbojpeg, libwebp +
Optional dependencies: lcms2, LibRaw, librsvg-2.0, xcursor, libheif, libtiff,
ExifTool, resvg (unstable API, needs to be requested explicitly) +
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/fiv.git
$ meson builddir
$ cd fiv
$ meson setup builddir
$ cd builddir
$ meson compile
Considering the vast amount of dynamically-linked dependencies, do not attempt
direct installations via `ninja install`. To test the program:
$ meson devenv fiv
The lossless JPEG cropper and reverse image search are intended to be invoked
from a context menu.
from a file manager context menu.
For proper integration, you will need to install the application. On Debian,
you can get a quick and dirty installation package for testing purposes using:
$ meson compile deb
# dpkg -i fiv-*.deb
Windows
~~~~~~~
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
dependencies listed at the beginning of 'msys2-cross-configure.sh',
plus rsvg-convert from librsvg2, and icotool from icoutils.
dependencies listed at the beginning of 'msys2-configure.sh',
plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
Beware that the build will take up about a gigabyte of disk space.
$ sh -e msys2-cross-configure.sh builddir
$ meson install -C builddir
$ sh -e msys2-configure.sh builddir
$ meson compile package -C builddir
If everything succeeds, you will find a portable build of the application
in the 'builddir/package' subdirectory. Keep your expectations low.
in the 'builddir/package' subdirectory, and a very basic MSI installation
package in 'builddir'.
Faster colour management
^^^^^^^^^^^^^^^^^^^^^^^^
To get the Little CMS fast float plugin, you'll have to enter MSYS2 and
https://www.msys2.org/wiki/Creating-Packages/#re-building-a-package[rebuild]
_mingw-w64-lcms2_ with the following change:
sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
Documentation
-------------

View File

@@ -26,6 +26,19 @@ the _User Guide_.
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.
@@ -35,18 +48,11 @@ Options
the list of MIME types within *fiv*'s desktop file when the list
of GdkPixbuf loaders changes.
*--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.
*--thumbnail*=_SIZE_::
Generate 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_.
*-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*
@@ -54,11 +60,16 @@ Options
exiting early if successful. This is used to enhance responsivity
of thumbnail procurement.
*-V*, *--version*::
Output version information and exit.
*--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_.
*--help-all*::
Show the full list of options, including those provided by GTK+.
*--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
--------------

View File

@@ -16,10 +16,10 @@ q:lang(en):after { content: ""; }
<p class="details">
<span id="author">Přemysl Eric Janouch</span><br>
<span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br>
<span id="revnumber">version 0.0.0,</span>
<span id="revdate">2022-07-31</span>
<span id="revnumber">version 1.0.0,</span>
<span id="revdate">2023-04-17</span>
<p class="figure"><img src="fiv.webp" alt="fiv's browser and viewer">
<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
<h2>Introduction</h2>
@@ -33,30 +33,31 @@ 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 Ctrl+?
will give you a convenient overview of all shortcuts. In addition to these,
remember that you may often use Ctrl+Tab and F6 to navigate to different groups
of widgets.
<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, view controls,
and finally breadcrumbs leading to the currently opened directory, as well as
its descendants.
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 either
directly, or with the Ctrl key pressed down. Right clicking the directory view
offers a context menu for opening files, or even the directory itself,
in a different application.
<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 Enter 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.
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.
@@ -94,14 +95,8 @@ rm -rf ~/.cache/thumbnails/wide-*
<h2>Configuration</h2>
<p>The few configuration options <i>fiv</i> has can be adjusted using
<i>dconf-editor</i>, which can be launched in the appropriate location from
within the application by pressing Ctrl+,. For command line usage, there is
the <i>gsettings</i> utility:
<pre>
gsettings list-recursively name.janouch.fiv
</pre>
<p>To adjust the few configuration options of <i>fiv</i>,
press <kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>,</kbd> to open <i>Preferences</i>.
<p>To make your changes take effect, restart <i>fiv</i>.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

@@ -5,4 +5,5 @@ 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; }
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

@@ -1,7 +1,7 @@
//
// fiv-browser.c: filesystem browsing widget
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2025, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -17,9 +17,6 @@
#include "config.h"
#include <math.h>
#include <pixman.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@@ -27,11 +24,16 @@
#ifdef GDK_WINDOWING_QUARTZ
#include <gdk/gdkquartz.h>
#endif // GDK_WINDOWING_QUARTZ
#include <pixman.h>
#include <math.h>
#include <stdlib.h>
#include "fiv-browser.h"
#include "fiv-collection.h"
#include "fiv-context-menu.h"
#include "fiv-io.h"
#include "fiv-io-model.h"
#include "fiv-thumbnail.h"
// --- Widget ------------------------------------------------------------------
@@ -78,7 +80,7 @@ struct _FivBrowser {
gboolean show_labels; ///< Show labels underneath items
FivIoModel *model; ///< Filesystem model
GArray *entries; ///< []Entry
GPtrArray *entries; ///< []*Entry
GArray *layouted_rows; ///< []Row
const Entry *selected; ///< Selected entry or NULL
@@ -90,7 +92,8 @@ struct _FivBrowser {
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
size_t thumbnailers_len; ///< Thumbnailers array size
GQueue thumbnailers_queue; ///< Queued up Entry pointers
GQueue thumbnailers_queue_1; ///< Queued up Entry pointers, hi-prio
GQueue thumbnailers_queue_2; ///< Queued up Entry pointers, lo-prio
GdkCursor *pointer; ///< Cached pointer cursor
cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners
@@ -105,26 +108,32 @@ struct _FivBrowser {
/// The "last modified" timestamp of source images for thumbnails.
static cairo_user_data_key_t fiv_browser_key_mtime_msec;
/// The original file size of source images for thumbnails.
static cairo_user_data_key_t fiv_browser_key_filesize;
// TODO(p): Include FivIoModelEntry data by reference.
struct entry {
gchar *uri; ///< GIO URI
gchar *target_uri; ///< GIO URI for any target
gchar *display_name; ///< Label for the file
gint64 mtime_msec; ///< Modification time in milliseconds
FivIoModelEntry *e; ///< Reference to model entry
cairo_surface_t *thumbnail; ///< Prescaled thumbnail
GIcon *icon; ///< If no thumbnail, use this icon
gboolean removed; ///< Model announced removal
};
static void
entry_free(Entry *self)
static Entry *
entry_new(FivIoModelEntry *e)
{
g_free(self->uri);
g_free(self->target_uri);
g_free(self->display_name);
Entry *self = g_slice_alloc0(sizeof *self);
self->e = e;
return self;
}
static void
entry_destroy(Entry *self)
{
fiv_io_model_entry_unref(self->e);
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
g_clear_object(&self->icon);
g_slice_free1(sizeof *self, self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -200,9 +209,6 @@ relayout(FivBrowser *self, int width)
gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
int available_width = width - padding.left - padding.right, max_width = 0;
// TODO(p): Remember the first visible item and the vertical offset into it,
// then try to ensure its visibility at the end (useful for reloads).
g_array_set_size(self->layouted_rows, 0);
// Whatever self->drag_begin_* used to point at might no longer be there,
// but thumbnail reloading would disrupt mouse clicks if we cleared them.
@@ -210,7 +216,7 @@ relayout(FivBrowser *self, int width)
GArray *items = g_array_new(TRUE, TRUE, sizeof(Item));
int x = 0, y = padding.top;
for (guint i = 0; i < self->entries->len; i++) {
const Entry *entry = &g_array_index(self->entries, Entry, i);
const Entry *entry = self->entries->pdata[i];
if (!entry->thumbnail)
continue;
@@ -228,17 +234,20 @@ relayout(FivBrowser *self, int width)
PangoLayout *label = NULL;
if (self->show_labels) {
label = gtk_widget_create_pango_layout(widget, entry->display_name);
label = gtk_widget_create_pango_layout(
widget, entry->e->display_name);
pango_layout_set_width(
label, (width - 2 * self->glow_w) * PANGO_SCALE);
pango_layout_set_alignment(label, PANGO_ALIGN_CENTER);
pango_layout_set_wrap(label, PANGO_WRAP_WORD_CHAR);
pango_layout_set_ellipsize(label, PANGO_ELLIPSIZE_END);
#if PANGO_VERSION_CHECK(1, 44, 0)
PangoAttrList *attrs = pango_attr_list_new();
pango_attr_list_insert(attrs, pango_attr_insert_hyphens_new(FALSE));
pango_layout_set_attributes(label, attrs);
pango_attr_list_unref (attrs);
#endif
}
g_array_append_val(items, ((Item) {
@@ -267,14 +276,13 @@ relayout(FivBrowser *self, int width)
gtk_adjustment_set_page_size(self->hadjustment, width);
}
if (self->vadjustment) {
int height = gtk_widget_get_allocated_height(widget);
gtk_adjustment_set_lower(self->vadjustment, 0);
gtk_adjustment_set_upper(self->vadjustment, total_height);
gtk_adjustment_set_upper(self->vadjustment, MAX(height, total_height));
gtk_adjustment_set_step_increment(self->vadjustment,
self->item_height + self->item_spacing + 2 * self->item_border_y);
gtk_adjustment_set_page_increment(
self->vadjustment, gtk_widget_get_allocated_height(widget) * 0.9);
gtk_adjustment_set_page_size(
self->vadjustment, gtk_widget_get_allocated_height(widget));
gtk_adjustment_set_page_increment(self->vadjustment, height * 0.9);
gtk_adjustment_set_page_size(self->vadjustment, height);
}
return total_height;
}
@@ -397,7 +405,7 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row)
// Performance optimization--specifically targeting the checkerboard.
if (cairo_image_surface_get_format(item->entry->thumbnail) !=
CAIRO_FORMAT_RGB24) {
CAIRO_FORMAT_RGB24 || item->entry->removed) {
gtk_render_background(style, cr, border.left, border.top,
extents.width, extents.height);
}
@@ -413,12 +421,39 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row)
cairo_mask_surface(
cr, item->entry->thumbnail, border.left, border.top);
} else {
// Distinguish removed items by rendering them only faintly.
if (item->entry->removed)
cairo_push_group(cr);
cairo_set_source_surface(
cr, item->entry->thumbnail, border.left, border.top);
cairo_paint(cr);
// Here, we could consider multiplying
// Here, we could also consider multiplying
// the whole rectangle with the selection color.
if (item->entry->removed) {
cairo_pop_group_to_source(cr);
cairo_paint_with_alpha(cr, 0.25);
}
}
// This rendition is about the best I could come up with.
// It might be possible to use more such emblems with entries,
// though they would deserve some kind of a blur-glow.
if (item->entry->removed) {
int size = 32;
cairo_surface_t *cross = gtk_icon_theme_load_surface(
gtk_icon_theme_get_default(), "cross-large-symbolic",
size, gtk_widget_get_scale_factor(GTK_WIDGET(self)),
gtk_widget_get_window(GTK_WIDGET(self)),
GTK_ICON_LOOKUP_FORCE_SYMBOLIC, NULL);
if (cross) {
cairo_set_source_rgb(cr, 1, 0, 0);
cairo_mask_surface(cr, cross,
border.left + extents.width - size - size / 4,
border.top + extents.height - size - size / 4);
cairo_surface_destroy(cross);
}
}
if (self->show_labels) {
@@ -505,51 +540,81 @@ rescale_thumbnail(cairo_surface_t *thumbnail, double row_height)
return scaled;
}
static char *
static const char *
entry_system_wide_uri(const Entry *self)
{
// "recent" and "trash", e.g., also have "standard::target-uri" set,
// but we'd like to avoid saving their thumbnails.
if (self->target_uri && fiv_collection_uri_matches(self->uri))
return self->target_uri;
if (self->e->target_uri && fiv_collection_uri_matches(self->e->uri))
return self->e->target_uri;
return self->uri;
return self->e->uri;
}
static void
entry_set_surface_user_data(const Entry *self)
{
// This choice of mtime favours unnecessary thumbnail reloading
// over retaining stale data (consider both calling functions).
cairo_surface_set_user_data(self->thumbnail,
&fiv_browser_key_mtime_msec, (void *) (intptr_t) self->e->mtime_msec,
NULL);
cairo_surface_set_user_data(self->thumbnail,
&fiv_browser_key_filesize, (void *) (uintptr_t) self->e->filesize,
NULL);
}
static cairo_surface_t *
entry_lookup_thumbnail(Entry *self, FivBrowser *browser)
{
cairo_surface_t *cached =
g_hash_table_lookup(browser->thumbnail_cache, self->e->uri);
if (cached &&
(intptr_t) cairo_surface_get_user_data(cached,
&fiv_browser_key_mtime_msec) == (intptr_t) self->e->mtime_msec &&
(uintptr_t) cairo_surface_get_user_data(cached,
&fiv_browser_key_filesize) == (uintptr_t) self->e->filesize) {
// TODO(p): If this hit is low-quality, see if a high-quality thumbnail
// hasn't been produced without our knowledge (avoid launching a minion
// unnecessarily; we might also shift the concern there).
return cairo_surface_reference(cached);
}
cairo_surface_t *found = fiv_thumbnail_lookup(
entry_system_wide_uri(self), self->e->mtime_msec, self->e->filesize,
browser->item_size);
return rescale_thumbnail(found, browser->item_height);
}
static void
entry_add_thumbnail(gpointer data, gpointer user_data)
{
Entry *self = data;
FivBrowser *browser = FIV_BROWSER(user_data);
if (self->removed) {
// Keep whatever size of thumbnail we had at the time up until reload.
// g_file_query_info() fails for removed files, so keep the icon, too.
if (self->icon) {
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
} else {
self->thumbnail =
rescale_thumbnail(self->thumbnail, browser->item_height);
}
return;
}
g_clear_object(&self->icon);
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
FivBrowser *browser = FIV_BROWSER(user_data);
cairo_surface_t *cached =
g_hash_table_lookup(browser->thumbnail_cache, self->uri);
if (cached &&
(intptr_t) cairo_surface_get_user_data(
cached, &fiv_browser_key_mtime_msec) == self->mtime_msec) {
self->thumbnail = cairo_surface_reference(cached);
// TODO(p): If this hit is low-quality, see if a high-quality thumbnail
// hasn't been produced without our knowledge (avoid launching a minion
// unnecessarily; we might also shift the concern there).
} else {
cairo_surface_t *found = fiv_thumbnail_lookup(
entry_system_wide_uri(self), self->mtime_msec, browser->item_size);
self->thumbnail = rescale_thumbnail(found, browser->item_height);
}
if (self->thumbnail) {
// This choice of mtime favours unnecessary thumbnail reloading.
cairo_surface_set_user_data(self->thumbnail,
&fiv_browser_key_mtime_msec, (void *) (intptr_t) self->mtime_msec,
NULL);
if ((self->thumbnail = entry_lookup_thumbnail(self, browser))) {
// Yes, this is a pointless action in case it's been found in the cache.
entry_set_surface_user_data(self);
return;
}
// Fall back to symbolic icons, though there's only so much we can do
// in parallel--GTK+ isn't thread-safe.
GFile *file = g_file_new_for_uri(self->uri);
GFile *file = g_file_new_for_uri(self->e->uri);
GFileInfo *info = g_file_query_info(file,
G_FILE_ATTRIBUTE_STANDARD_NAME
"," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
@@ -578,11 +643,15 @@ materialize_icon(FivBrowser *self, Entry *entry)
// of using GLib to look up icons for us, derive a list from a guessed
// MIME type, with "-symbolic" prefixes and fallbacks,
// and use gtk_icon_theme_choose_icon() instead.
// TODO(p): Make sure we have /some/ icon for every entry.
// TODO(p): We might want to populate these on an as-needed basis.
GtkIconInfo *icon_info = gtk_icon_theme_lookup_by_gicon(
gtk_icon_theme_get_default(), entry->icon, self->item_height / 2,
GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
GtkIconTheme *theme = gtk_icon_theme_get_default();
GtkIconInfo *icon_info = gtk_icon_theme_lookup_by_gicon(theme, entry->icon,
self->item_height / 2, GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
if (!icon_info) {
// This icon is included within GTK+.
icon_info = gtk_icon_theme_lookup_icon(theme, "text-x-generic",
self->item_height / 2, GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
}
if (!icon_info)
return;
@@ -611,27 +680,39 @@ materialize_icon(FivBrowser *self, Entry *entry)
g_object_unref(icon_info);
}
static void
reload_one_thumbnail_finish(FivBrowser *self, Entry *entry)
{
if (!entry->removed && entry->thumbnail) {
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri),
cairo_surface_reference(entry->thumbnail));
}
materialize_icon(self, entry);
}
static void
reload_one_thumbnail(FivBrowser *self, Entry *entry)
{
entry_add_thumbnail(entry, self);
reload_one_thumbnail_finish(self, entry);
gtk_widget_queue_resize(GTK_WIDGET(self));
}
static void
reload_thumbnails(FivBrowser *self)
{
GThreadPool *pool = g_thread_pool_new(
entry_add_thumbnail, self, g_get_num_processors(), FALSE, NULL);
for (guint i = 0; i < self->entries->len; i++)
g_thread_pool_push(pool, &g_array_index(self->entries, Entry, i), NULL);
g_thread_pool_push(pool, self->entries->pdata[i], NULL);
g_thread_pool_free(pool, FALSE, TRUE);
// Once a URI disappears from the model, its thumbnail is forgotten.
g_hash_table_remove_all(self->thumbnail_cache);
for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = &g_array_index(self->entries, Entry, i);
if (entry->thumbnail) {
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri),
cairo_surface_reference(entry->thumbnail));
}
materialize_icon(self, entry);
}
for (guint i = 0; i < self->entries->len; i++)
reload_one_thumbnail_finish(self, self->entries->pdata[i]);
gtk_widget_queue_resize(GTK_WIDGET(self));
}
@@ -662,15 +743,11 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
(void *) (intptr_t) 1, NULL);
g_queue_push_tail(&self->thumbnailers_queue, entry);
g_queue_push_tail(&self->thumbnailers_queue_2, entry);
}
// This choice of mtime favours unnecessary thumbnail reloading
// over retaining stale data.
cairo_surface_set_user_data(entry->thumbnail,
&fiv_browser_key_mtime_msec, (void *) (intptr_t) entry->mtime_msec,
NULL);
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri),
entry_set_surface_user_data(entry);
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri),
cairo_surface_reference(entry->thumbnail));
}
@@ -720,13 +797,21 @@ on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
thumbnailer_next(t);
}
// TODO(p): Try to keep the minions alive (stdout will be a problem).
static gboolean
thumbnailer_next(Thumbnailer *t)
{
// TODO(p): Try to keep the minions alive (stdout will be a problem).
// Already have something to do, not a failure.
if (t->target)
return TRUE;
// They could have been removed via post-reload changes in the model.
FivBrowser *self = t->self;
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue)))
return FALSE;
do {
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue_1)) &&
!(t->target = g_queue_pop_head(&self->thumbnailers_queue_2)))
return FALSE;
} while (t->target->removed);
// Case analysis:
// - We haven't found any thumbnail for the entry at all
@@ -743,9 +828,18 @@ thumbnailer_next(Thumbnailer *t)
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
"--", uri, NULL};
GSubprocessLauncher *launcher =
g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE);
#ifdef G_OS_WIN32
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
g_subprocess_launcher_set_cwd(launcher, prefix);
g_free(prefix);
#endif
GError *error = NULL;
t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower,
G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
t->minion = g_subprocess_launcher_spawnv(
launcher, t->target->icon ? argv_faster : argv_slower, &error);
g_object_unref(launcher);
if (error) {
g_warning("%s", error->message);
g_error_free(error);
@@ -763,7 +857,8 @@ thumbnailer_next(Thumbnailer *t)
static void
thumbnailers_abort(FivBrowser *self)
{
g_queue_clear(&self->thumbnailers_queue);
g_queue_clear(&self->thumbnailers_queue_1);
g_queue_clear(&self->thumbnailers_queue_2);
for (size_t i = 0; i < self->thumbnailers_len; i++) {
Thumbnailer *t = self->thumbnailers + i;
@@ -779,32 +874,35 @@ thumbnailers_abort(FivBrowser *self)
}
static void
thumbnailers_start(FivBrowser *self)
thumbnailers_enqueue(FivBrowser *self, Entry *entry)
{
thumbnailers_abort(self);
if (!self->model)
return;
GQueue lq = G_QUEUE_INIT;
for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = &g_array_index(self->entries, Entry, i);
if (!entry->removed) {
if (entry->icon)
g_queue_push_tail(&self->thumbnailers_queue, entry);
g_queue_push_tail(&self->thumbnailers_queue_1, entry);
else if (cairo_surface_get_user_data(
entry->thumbnail, &fiv_thumbnail_key_lq))
g_queue_push_tail(&lq, entry);
}
while (!g_queue_is_empty(&lq)) {
g_queue_push_tail_link(
&self->thumbnailers_queue, g_queue_pop_head_link(&lq));
g_queue_push_tail(&self->thumbnailers_queue_2, entry);
}
}
static void
thumbnailers_deploy(FivBrowser *self)
{
for (size_t i = 0; i < self->thumbnailers_len; i++) {
if (!thumbnailer_next(self->thumbnailers + i))
break;
}
}
static void
thumbnailers_restart(FivBrowser *self)
{
thumbnailers_abort(self);
for (guint i = 0; i < self->entries->len; i++)
thumbnailers_enqueue(self, self->entries->pdata[i]);
thumbnailers_deploy(self);
}
// --- Boilerplate -------------------------------------------------------------
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
@@ -865,7 +963,7 @@ fiv_browser_finalize(GObject *gobject)
{
FivBrowser *self = FIV_BROWSER(gobject);
thumbnailers_abort(self);
g_array_free(self->entries, TRUE);
g_ptr_array_free(self->entries, TRUE);
g_array_free(self->layouted_rows, TRUE);
if (self->model) {
g_signal_handlers_disconnect_by_data(self->model, self);
@@ -925,7 +1023,7 @@ set_item_size(FivBrowser *self, FivThumbnailSize size)
g_hash_table_remove_all(self->thumbnail_cache);
reload_thumbnails(self);
thumbnailers_start(self);
thumbnailers_restart(self);
g_object_notify_by_pspec(
G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
@@ -1118,7 +1216,7 @@ fiv_browser_draw(GtkWidget *widget, cairo_t *cr)
static gboolean
open_entry(GtkWidget *self, const Entry *entry, gboolean new_window)
{
GFile *location = g_file_new_for_uri(entry->uri);
GFile *location = g_file_new_for_uri(entry->e->uri);
g_signal_emit(self, browser_signals[ITEM_ACTIVATED], 0, location,
new_window ? GTK_PLACES_OPEN_NEW_WINDOW : GTK_PLACES_OPEN_NORMAL);
g_object_unref(location);
@@ -1128,7 +1226,9 @@ open_entry(GtkWidget *self, const Entry *entry, gboolean new_window)
static void
show_context_menu(GtkWidget *widget, GFile *file)
{
gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, file), NULL);
GtkMenu *menu = fiv_context_menu_new(widget, file);
if (menu)
gtk_menu_popup_at_pointer(menu, NULL);
}
static void
@@ -1188,7 +1288,7 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
// no matter what its new location is.
gdk_window_set_cursor(gtk_widget_get_window(widget), NULL);
GFile *file = g_file_new_for_uri(entry->uri);
GFile *file = g_file_new_for_uri(entry->e->uri);
show_context_menu(widget, file);
g_object_unref(file);
return GDK_EVENT_STOP;
@@ -1223,10 +1323,14 @@ fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
if (!entry || entry != entry_at(self, event->x, event->y))
return GDK_EVENT_PROPAGATE;
GdkModifierType primary = gdk_keymap_get_modifier_mask(
gdk_keymap_get_for_display(gtk_widget_get_display(widget)),
GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
guint state = event->state & gtk_accelerator_get_default_mod_mask();
if ((event->button == GDK_BUTTON_PRIMARY && state == 0))
return open_entry(widget, entry, FALSE);
if ((event->button == GDK_BUTTON_PRIMARY && state == GDK_CONTROL_MASK) ||
if ((event->button == GDK_BUTTON_PRIMARY && state == primary) ||
(event->button == GDK_BUTTON_MIDDLE && state == 0))
return open_entry(widget, entry, TRUE);
return GDK_EVENT_PROPAGATE;
@@ -1332,8 +1436,8 @@ fiv_browser_drag_data_get(GtkWidget *widget,
{
FivBrowser *self = FIV_BROWSER(widget);
if (self->selected) {
(void) gtk_selection_data_set_uris(
data, (gchar *[]) {entry_system_wide_uri(self->selected), NULL});
(void) gtk_selection_data_set_uris(data, (gchar *[])
{(gchar *) entry_system_wide_uri(self->selected), NULL});
}
}
@@ -1481,6 +1585,14 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
case 0:
switch (event->keyval) {
case GDK_KEY_Delete:
if (self->selected) {
GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
GFile *file = g_file_new_for_uri(self->selected->e->uri);
fiv_context_menu_remove(window, file);
g_object_unref(file);
}
return GDK_EVENT_STOP;
case GDK_KEY_Return:
if (self->selected)
return open_entry(widget, self->selected, FALSE);
@@ -1510,7 +1622,7 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
case GDK_KEY_Return:
if (self->selected) {
GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
fiv_context_menu_information(window, self->selected->uri);
fiv_context_menu_information(window, self->selected->e->uri);
}
return GDK_EVENT_STOP;
}
@@ -1545,7 +1657,7 @@ fiv_browser_query_tooltip(GtkWidget *widget, gint x, gint y,
if (!entry)
return FALSE;
gtk_tooltip_set_text(tooltip, entry->display_name);
gtk_tooltip_set_text(tooltip, entry->e->display_name);
return TRUE;
}
@@ -1559,7 +1671,7 @@ fiv_browser_popup_menu(GtkWidget *widget)
GFile *file = NULL;
GdkRectangle rect = {};
if (self->selected) {
file = g_file_new_for_uri(self->selected->uri);
file = g_file_new_for_uri(self->selected->e->uri);
rect = entry_rect(self, self->selected);
rect.x += rect.width / 2;
rect.y += rect.height / 2;
@@ -1597,7 +1709,7 @@ on_long_press(GtkGestureLongPress *lp, gdouble x, gdouble y, gpointer user_data)
// It might also be possible to have long-press just select items,
// and show some kind of toolbar with available actions.
GFile *file = g_file_new_for_uri(entry->uri);
GFile *file = g_file_new_for_uri(entry->e->uri);
gtk_menu_popup_at_rect(fiv_context_menu_new(widget, file), window,
&(GdkRectangle) {.x = x, .y = y}, GDK_GRAVITY_NORTH_WEST,
GDK_GRAVITY_NORTH_WEST, event);
@@ -1769,8 +1881,8 @@ fiv_browser_init(FivBrowser *self)
gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
gtk_widget_set_has_tooltip(GTK_WIDGET(self), TRUE);
self->entries = g_array_new(FALSE, TRUE, sizeof(Entry));
g_array_set_clear_func(self->entries, (GDestroyNotify) entry_free);
self->entries =
g_ptr_array_new_with_free_func((GDestroyNotify) entry_destroy);
self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row));
g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free);
abort_button_tracking(self);
@@ -1783,7 +1895,8 @@ fiv_browser_init(FivBrowser *self)
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
for (size_t i = 0; i < self->thumbnailers_len; i++)
self->thumbnailers[i].self = self;
g_queue_init(&self->thumbnailers_queue);
g_queue_init(&self->thumbnailers_queue_1);
g_queue_init(&self->thumbnailers_queue_2);
set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL);
self->show_labels = FALSE;
@@ -1805,36 +1918,80 @@ fiv_browser_init(FivBrowser *self)
// --- Public interface --------------------------------------------------------
// TODO(p): Later implement any arguments of this FivIoModel signal.
static void
on_model_files_changed(FivIoModel *model, FivBrowser *self)
on_model_reloaded(FivIoModel *model, FivBrowser *self)
{
g_return_if_fail(model == self->model);
gchar *selected_uri = NULL;
if (self->selected)
selected_uri = g_strdup(self->selected->uri);
selected_uri = g_strdup(self->selected->e->uri);
thumbnailers_abort(self);
g_array_set_size(self->entries, 0);
g_array_set_size(self->layouted_rows, 0);
g_ptr_array_set_size(self->entries, 0);
gsize len = 0;
const FivIoModelEntry *files = fiv_io_model_get_files(self->model, &len);
FivIoModelEntry *const *files = fiv_io_model_get_files(self->model, &len);
for (gsize i = 0; i < len; i++) {
Entry e = {.thumbnail = NULL,
.uri = g_strdup(files[i].uri),
.target_uri = g_strdup(files[i].target_uri),
.display_name = g_strdup(files[i].display_name),
.mtime_msec = files[i].mtime_msec};
g_array_append_val(self->entries, e);
g_ptr_array_add(
self->entries, entry_new(fiv_io_model_entry_ref(files[i])));
}
fiv_browser_select(self, selected_uri);
g_free(selected_uri);
// Restarting thumbnailers is critical, because they keep Entry pointers.
reload_thumbnails(self);
thumbnailers_start(self);
thumbnailers_restart(self);
}
static void
on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
FivBrowser *self)
{
g_return_if_fail(model == self->model);
// Add new entries to the end, so as to not disturb the layout.
if (!old) {
Entry *entry = entry_new(fiv_io_model_entry_ref(new));
g_ptr_array_add(self->entries, entry);
reload_one_thumbnail(self, entry);
thumbnailers_enqueue(self, entry);
thumbnailers_deploy(self);
return;
}
Entry *found = NULL;
for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = self->entries->pdata[i];
if (entry->e == old) {
found = entry;
break;
}
}
if (!found)
return;
// Rename entries in place, so as to not disturb the layout.
// XXX: This behaves differently from FivIoModel, and by extension fiv.c.
if (new) {
fiv_io_model_entry_unref(found->e);
found->e = fiv_io_model_entry_ref(new);
found->removed = FALSE;
// TODO(p): If there is a URI mismatch, don't reload thumbnails,
// so that there's no jumping around. Or, a bit more properly,
// move the thumbnail cache entry to the new URI.
reload_one_thumbnail(self, found);
// TODO(p): Rather cancel the entry in any running thumbnailer,
// remove it from queues, and _enqueue() + _deploy().
thumbnailers_restart(self);
} else {
found->removed = TRUE;
gtk_widget_queue_draw(GTK_WIDGET(self));
}
}
GtkWidget *
@@ -1845,9 +2002,11 @@ fiv_browser_new(FivIoModel *model)
FivBrowser *self = g_object_new(FIV_TYPE_BROWSER, NULL);
self->model = g_object_ref(model);
g_signal_connect(
self->model, "files-changed", G_CALLBACK(on_model_files_changed), self);
on_model_files_changed(self->model, self);
g_signal_connect(self->model, "reloaded",
G_CALLBACK(on_model_reloaded), self);
g_signal_connect(self->model, "files-changed",
G_CALLBACK(on_model_changed), self);
on_model_reloaded(self->model, self);
return GTK_WIDGET(self);
}
@@ -1876,8 +2035,8 @@ fiv_browser_select(FivBrowser *self, const char *uri)
return;
for (guint i = 0; i < self->entries->len; i++) {
const Entry *entry = &g_array_index(self->entries, Entry, i);
if (!g_strcmp0(entry->uri, uri)) {
const Entry *entry = self->entries->pdata[i];
if (!g_strcmp0(entry->e->uri, uri)) {
self->selected = entry;
scroll_to_selection(self);
break;

View File

@@ -17,7 +17,7 @@
#pragma once
#include "fiv-io.h"
#include "fiv-io-model.h"
#include <gtk/gtk.h>

View File

@@ -528,12 +528,16 @@ fiv_collection_file_query_info(GFile *file, const char *attributes,
g_file_info_set_name(info, basename);
g_free(basename);
if ((name = g_file_info_get_display_name(info))) {
if (g_file_info_has_attribute(
info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) &&
(name = g_file_info_get_display_name(info))) {
gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_display_name(info, prefixed);
g_free(prefixed);
}
if ((name = g_file_info_get_edit_name(info))) {
if (g_file_info_has_attribute(
info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME) &&
(name = g_file_info_get_edit_name(info))) {
gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_edit_name(info, prefixed);
g_free(prefixed);

View File

@@ -1,7 +1,7 @@
//
// fiv-context-menu.c: popup menu
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -185,15 +185,24 @@ info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
if (bytes_in)
flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
GSubprocessLauncher *launcher = g_subprocess_launcher_new(flags);
#ifdef G_OS_WIN32
// Both to find wperl, and then to let wperl find the nearby exiftool.
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
g_subprocess_launcher_set_cwd(launcher, prefix);
g_free(prefix);
#endif
// TODO(p): Add a fallback to internal capabilities.
// The simplest is to specify the filename and the resolution.
GError *error = NULL;
GSubprocess *subprocess = g_subprocess_new(flags, &error,
GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
#ifdef G_OS_WIN32
"wperl",
#endif
"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
"--binary", "-quiet", "--", path, NULL);
g_object_unref(launcher);
if (error) {
info_redirect_error(dialog, error);
return;
@@ -327,6 +336,17 @@ 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)
{
@@ -342,8 +362,9 @@ open_context_launch(GtkWidget *widget, OpenContext *self)
(void) g_app_info_set_as_last_used_for_type(
self->app_info, self->content_type, NULL);
} else {
g_warning("%s", error->message);
g_error_free(error);
GtkWindow *window = g_weak_ref_get(&self->window);
show_error_dialog(window, error);
g_clear_object(&window);
}
g_list_free(files);
g_object_unref(context);
@@ -423,6 +444,24 @@ on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
g_free(uri);
}
void
fiv_context_menu_remove(GtkWindow *parent, GFile *file)
{
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
GError *error = NULL;
if (!g_file_trash(file, NULL, &error))
show_error_dialog(parent, error);
}
static void
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
{
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)
{
@@ -503,11 +542,21 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
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...");
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);

View File

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

463
fiv-io-cmm.c Normal file
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;
}

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

@@ -0,0 +1,743 @@
//
// 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:
// On macOS, we seem to not receive _CHANGED for child files.
// And while this seems to arrive too early, it's a mild improvement.
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
event = MONITOR_CHANGING;
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_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);

2795
fiv-io.c

File diff suppressed because it is too large Load Diff

285
fiv-io.h
View File

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

View File

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

View File

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

View File

@@ -312,28 +312,9 @@ on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res,
}
static void
update_location(FivSidebar *self)
reload_directories(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);
gtk_container_foreach(GTK_CONTAINER(self->listbox),
(GtkCallback) gtk_widget_destroy, NULL);
if (!location)
@@ -357,16 +338,51 @@ update_location(FivSidebar *self)
gtk_container_add(GTK_CONTAINER(self->listbox), row);
gsize len = 0;
const FivIoModelEntry *subdirs =
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);
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)
@@ -417,7 +433,10 @@ complete_path(GFile *location, GtkListStore *model)
!info)
break;
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY ||
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY)
continue;
if (g_file_info_has_attribute(info,
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
g_file_info_get_is_hidden(info))
continue;
@@ -518,6 +537,13 @@ on_show_enter_location(
g_signal_connect(entry, "changed",
G_CALLBACK(on_enter_location_changed), self);
GFile *location = fiv_io_model_get_location(self->model);
if (location) {
gchar *parse_name = g_file_get_parse_name(location);
gtk_entry_set_text(GTK_ENTRY(entry), parse_name);
g_free(parse_name);
}
// Can't have it ellipsized and word-wrapped at the same time.
GtkWidget *protocols = gtk_label_new("");
gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);
@@ -623,10 +649,11 @@ fiv_sidebar_new(FivIoModel *model)
gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port),
gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self)));
// TODO(p): There should be an extra signal to watch location changes only.
self->model = g_object_ref(model);
g_signal_connect_swapped(self->model, "subdirectories-changed",
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);
}

View File

@@ -17,7 +17,7 @@
#pragma once
#include "fiv-io.h"
#include "fiv-io-model.h"
#include <gtk/gtk.h>

View File

@@ -100,7 +100,16 @@ mark_thumbnail_lq(cairo_surface_t *surface)
static gchar *
fiv_thumbnail_get_root(void)
{
#ifdef G_OS_WIN32
// We can do better than GLib with FOLDERID_InternetCache,
// and we don't want to place .cache directly in the user's home.
// TODO(p): Register this thumbnail path using the installer:
// https://learn.microsoft.com/en-us/windows/win32/lwef/disk-cleanup
gchar *cache_dir =
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
#else
gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
#endif
gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL);
g_free(cache_dir);
return thumbnails_dir;
@@ -125,35 +134,37 @@ might_be_a_thumbnail(const char *path_or_uri)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static cairo_surface_t *
static FivIoImage *
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
{
FivIoCmm *cmm = fiv_io_cmm_get_default();
FivIoOpenContext ctx = {
.uri = g_file_get_uri(target),
.screen_profile = fiv_io_profile_new_sRGB(),
// Remember to synchronize changes with adjust_thumbnail().
.cmm = cmm,
.screen_profile = fiv_io_cmm_get_profile_sRGB(cmm),
.screen_dpi = 96,
.first_frame_only = TRUE,
// Only using this array as a redirect.
.warnings = g_ptr_array_new_with_free_func(g_free),
};
cairo_surface_t *surface = fiv_io_open_from_data(
FivIoImage *image = fiv_io_open_from_data(
g_bytes_get_data(data, NULL), g_bytes_get_size(data), &ctx, error);
g_free((gchar *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE);
if ((*color_managed = !!ctx.screen_profile))
fiv_io_profile_free(ctx.screen_profile);
g_bytes_unref(data);
return surface;
return image;
}
// In principle similar to rescale_thumbnail() from fiv-browser.c.
static cairo_surface_t *
adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
static FivIoImage *
adjust_thumbnail(FivIoImage *thumbnail, double row_height)
{
// Hardcode orientation.
FivIoOrientation orientation = (uintptr_t) cairo_surface_get_user_data(
thumbnail, &fiv_io_key_orientation);
FivIoOrientation orientation = thumbnail->orientation;
double w = 0, h = 0;
cairo_matrix_t matrix =
@@ -170,33 +181,46 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
}
// Vector images should not have orientation, this should handle them all.
FivIoRenderClosure *closure =
cairo_surface_get_user_data(thumbnail, &fiv_io_key_render);
FivIoRenderClosure *closure = thumbnail->render;
if (closure && orientation <= FivIoOrientation0) {
// Remember to synchronize changes with render().
FivIoCmm *cmm = fiv_io_cmm_get_default();
FivIoProfile *screen_profile = fiv_io_cmm_get_profile_sRGB(cmm);
// This API doesn't accept non-uniform scaling; prefer a vertical fit.
cairo_surface_t *scaled = closure->render(closure, scale_y);
FivIoImage *scaled =
closure->render(closure, cmm, screen_profile, scale_y);
if (screen_profile)
fiv_io_profile_free(screen_profile);
if (scaled)
return scaled;
}
// This will be CAIRO_FORMAT_INVALID with non-image surfaces, which is fine.
cairo_format_t format = cairo_image_surface_get_format(thumbnail);
if (format != CAIRO_FORMAT_INVALID &&
orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
return cairo_surface_reference(thumbnail);
if (orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
return fiv_io_image_ref(thumbnail);
cairo_format_t format = thumbnail->format;
int projected_width = round(scale_x * w);
int projected_height = round(scale_y * h);
cairo_surface_t *scaled = cairo_image_surface_create(
FivIoImage *scaled = fiv_io_image_new(
(format == CAIRO_FORMAT_RGB24 || format == CAIRO_FORMAT_RGB30)
? CAIRO_FORMAT_RGB24
: CAIRO_FORMAT_ARGB32,
projected_width, projected_height);
if (!scaled) {
g_warning("image allocation failure");
return fiv_io_image_ref(thumbnail);
}
cairo_surface_t *surface = fiv_io_image_to_surface_noref(scaled);
cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
cairo_t *cr = cairo_create(scaled);
cairo_scale(cr, scale_x, scale_y);
cairo_set_source_surface(cr, thumbnail, 0, 0);
surface = fiv_io_image_to_surface_noref(thumbnail);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
cairo_pattern_t *pattern = cairo_get_source(cr);
// CAIRO_FILTER_BEST, for some reason, works bad with CAIRO_FORMAT_RGB30.
cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD);
@@ -208,9 +232,7 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
// Note that this doesn't get triggered with oversize input surfaces,
// even though nothing will be rendered.
if (cairo_surface_status(thumbnail) != CAIRO_STATUS_SUCCESS ||
cairo_surface_status(scaled) != CAIRO_STATUS_SUCCESS ||
cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
if (cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
cairo_status(cr) != CAIRO_STATUS_SUCCESS)
g_warning("thumbnail scaling failed");
@@ -218,28 +240,282 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
return scaled;
}
static cairo_surface_t *
orient_thumbnail(cairo_surface_t *surface, FivIoOrientation orientation)
static FivIoImage *
orient_thumbnail(FivIoImage *image)
{
if (!surface || orientation <= FivIoOrientation0)
return surface;
if (image->orientation <= FivIoOrientation0)
return image;
double w = 0, h = 0;
cairo_matrix_t matrix =
fiv_io_orientation_apply(surface, orientation, &w, &h);
cairo_surface_t *oriented =
cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
fiv_io_orientation_apply(image, image->orientation, &w, &h);
FivIoImage *oriented = fiv_io_image_new(image->format, w, h);
if (!oriented) {
g_warning("image allocation failure");
return image;
}
cairo_t *cr = cairo_create(oriented);
cairo_surface_t *surface = fiv_io_image_to_surface_noref(oriented);
cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
surface = fiv_io_image_to_surface(image);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
cairo_destroy(cr);
cairo_surface_destroy(surface);
return oriented;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ifdef HAVE_LIBRAW
#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
static int
extract_libraw_compare(const void *a, const void *b)
{
const libraw_thumbnail_item_t **t1 = (const libraw_thumbnail_item_t **) a;
const libraw_thumbnail_item_t **t2 = (const libraw_thumbnail_item_t **) b;
float p1 = (float) (*t1)->twidth * (*t1)->theight;
float p2 = (float) (*t2)->twidth * (*t2)->theight;
return (p2 < p1) - (p1 < p2);
}
static gboolean
extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
{
int count = iprc->thumbs_list.thumbcount;
if (count <= 0) {
set_error(error, "no thumbnails found");
return FALSE;
}
// The old libraw_unpack_thumb() goes for the largest thumbnail,
// but we currently want the smallest usable thumbnail. Order them.
libraw_thumbnail_item_t **sorted = g_malloc_n(count, sizeof *sorted);
for (int i = 0; i < count; i++)
sorted[i] = &iprc->thumbs_list.thumblist[i];
qsort(sorted, count, sizeof *sorted, extract_libraw_compare);
// With the raw.pixls.us database, zero dimensions occur in two cases:
// - when thumbcount should really be 0,
// - with the last, huge JPEG thumbnail in CR3 raws.
// The maintainer refuses to change anything about it (#589).
int i = 0;
while (i < count && (!sorted[i]->twidth || !sorted[i]->theight))
i++;
// Ignore thumbnails whose decoding is likely to be a waste of time.
// XXX: This primarily targets the TIFF/EP shortcut code,
// because decoding a thumbnail will always be /much/ quicker than a render.
// TODO(p): Maybe don't mark raw image thumbnails as low-quality
// if they're the right aspect ratio, and of sufficiently large size.
// The only downsides to camera-provided thumbnails seem to be cropping,
// and when they're decoded incorrectly. Also don't trust tflip.
float output_pixels = (float) iprc->sizes.iwidth * iprc->sizes.iheight;
// Note that the ratio may even be larger than 1, as seen with CR2 files.
while (i < count &&
(float) sorted[count - 1]->twidth * sorted[count - 1]->theight >
output_pixels * 0.75)
count--;
// The smallest size thumbnail is very often forced to be 4:3,
// and the remaining space is filled with black, looking quite wrong.
// It isn't really possible to strip those borders, because many are JPEGs.
//
// Another reason to skip thumbnails of mismatching aspect ratios is
// to avoid browser items from jumping around when low-quality thumbnails
// get replaced with their final versions.
//
// Note that some of them actually have borders on all four sides
// (Nikon/D50/DSC_5155.NEF, Nikon/D70/20170902_0047.NEF,
// Nikon/D70s/RAW_NIKON_D70S.NEF), or even on just one side
// (Leica/LEICA M MONOCHROM (Typ 246), Leica/M (Typ 240)).
// Another interesting possibility is Sony/DSC-HX99/DSC00001.ARW,
// where the correct-ratio thumbnail has borders but the main image doesn't.
//
// The problematic thumbnail is usually, but not always, sized 160x120,
// and some of them may actually be fine.
float output_ratio = (float) iprc->sizes.iwidth / iprc->sizes.iheight;
while (i < count) {
// XXX: tflip is less reliable than libraw_dcraw_make_mem_thumb()
// and reading out Orientation from the resulting Exif.
float ratio = sorted[i]->tflip == 5 || sorted[i]->tflip == 6
? (float) sorted[i]->theight / sorted[i]->twidth
: (float) sorted[i]->twidth / sorted[i]->theight;
if (fabsf(ratio - output_ratio) < 0.05)
break;
i++;
}
// Avoid pink-tinted readouts of CR2 IFD2 (#590).
//
// This thumbnail can also have a black stripe on the left and the top,
// which we should remove if using fixed LibRaw > 0.21.1.
if (i < count && iprc->idata.maker_index == LIBRAW_CAMERAMAKER_Canon &&
sorted[i]->tformat == LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB)
i++;
bool found = i != count;
if (found)
i = sorted[i] - iprc->thumbs_list.thumblist;
g_free(sorted);
if (!found) {
set_error(error, "no suitable thumbnails found");
return FALSE;
}
int err = 0;
if ((err = libraw_unpack_thumb_ex(iprc, i))) {
set_error(error, libraw_strerror(err));
return FALSE;
}
*flip = iprc->thumbs_list.thumblist[i].tflip;
return TRUE;
}
#else // LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
static gboolean
extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
{
int err = 0;
if ((err = libraw_unpack_thumb(iprc))) {
set_error(error, libraw_strerror(err));
return FALSE;
}
// The main image's "flip" often matches up, but sometimes doesn't, e.g.:
// - Phase One/H 25/H25_Outdoor_.IIQ
// - Phase One/H 25/H25_IT8.7-2_Card.TIF
*flip = iprc->sizes.flip;
return TRUE;
}
#endif // LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
// LibRaw does a weird permutation here, so follow the documentation,
// which assumes that mirrored orientations never happen.
static FivIoOrientation
extract_libraw_unflip(int flip)
{
switch (flip) {
break; case 0:
return FivIoOrientation0;
break; case 3:
return FivIoOrientation180;
break; case 5:
return FivIoOrientation270;
break; case 6:
return FivIoOrientation90;
break; default:
return FivIoOrientationUnknown;
}
}
static FivIoImage *
extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
{
// Anything else is extremely rare.
if (image->colors != 3 || image->bits != 8) {
set_error(error, "unsupported bitmap thumbnail");
return NULL;
}
FivIoImage *I = fiv_io_image_new(
CAIRO_FORMAT_RGB24, image->width, image->height);
if (!I) {
set_error(error, "image allocation failure");
return NULL;
}
guint32 *out = (guint32 *) I->data;
const unsigned char *in = image->data;
for (guint64 i = 0; i < (guint64) image->width * image->height; in += 3)
out[i++] = in[0] << 16 | in[1] << 8 | in[2];
I->orientation = extract_libraw_unflip(flip);
return I;
}
static FivIoImage *
extract_libraw(GFile *target, GMappedFile *mf, GError **error)
{
FivIoImage *I = NULL;
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;
}
int err = 0;
if ((err = libraw_open_buffer(iprc, (void *) g_mapped_file_get_contents(mf),
g_mapped_file_get_length(mf)))) {
set_error(error, libraw_strerror(err));
goto fail;
}
if ((err = libraw_adjust_sizes_info_only(iprc))) {
set_error(error, libraw_strerror(err));
goto fail;
}
int flip = 0;
if (!extract_libraw_unpack(iprc, &flip, error))
goto fail;
libraw_processed_image_t *image = libraw_dcraw_make_mem_thumb(iprc, &err);
if (!image) {
set_error(error, libraw_strerror(err));
goto fail;
}
// Bitmap thumbnails generally need rotating, e.g.:
// - Hasselblad/H4D-50/2-9-2017_street_0012.fff
// - OnePlus/One/IMG_20150729_201116.dng (and more DNGs in general)
//
// JPEG thumbnails generally have the right rotation in their Exif, e.g.:
// - Canon/EOS-1Ds Mark II/RAW_CANON_1DSM2.CR2
// - Leica/C (Typ 112)/Leica_-_C_(Typ_112)-_3:2.RWL
// - Nikon/1 S2/RAW_NIKON_1S2.NEF
// - Panasonic/DMC-FZ18/RAW_PANASONIC_LUMIX_FZ18.RAW
// - Panasonic/DMC-FZ70/P1000836.RW2
// - Samsung/NX200/2013-05-08-194524__sam6589.srw
// - Sony/DSC-HX95/DSC00018.ARW
// Note that LibRaw inserts its own Exif segment if it doesn't find one,
// and this may differ from flip. It may also be wrong, as in:
// - Leaf/Aptus 22/L_003172.mos
//
// Some files are problematic and we won't bother with special-casing:
// - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color.
// - Ricoh/GXR/R0017428.DNG (JPEG) seems to be plainly invalid.
switch (image->type) {
gboolean dummy;
case LIBRAW_IMAGE_JPEG:
I = render(
target, g_bytes_new(image->data, image->data_size), &dummy, error);
break;
case LIBRAW_IMAGE_BITMAP:
I = extract_libraw_bitmap(image, flip, error);
break;
default:
set_error(error, "unsupported embedded thumbnail");
}
libraw_dcraw_clear_mem(image);
fail:
libraw_close(iprc);
return I;
}
#endif // HAVE_LIBRAW
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cairo_surface_t *
fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
{
@@ -253,114 +529,36 @@ fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
if (!mf)
return NULL;
// Bitmap thumbnails generally need rotating, e.g.:
// - Hasselblad/H4D-50/2-9-2017_street_0012.fff
// - OnePlus/One/IMG_20150729_201116.dng (and more DNGs in general)
// Though it's apparent LibRaw doesn't adjust the thumbnails to match
// the main image's "flip" field (it just happens to match up often), e.g.:
// - Phase One/H 25/H25_Outdoor_.IIQ (correct Orientation in IFD0)
// - Phase One/H 25/H25_IT8.7-2_Card.TIF (correctly missing in IFD0)
//
// JPEG thumbnails generally have the right rotation in their Exif, e.g.:
// - Canon/EOS-1Ds Mark II/RAW_CANON_1DSM2.CR2
// - Leica/C (Typ 112)/Leica_-_C_(Typ_112)-_3:2.RWL
// - Nikon/1 S2/RAW_NIKON_1S2.NEF
// - Panasonic/DMC-FZ18/RAW_PANASONIC_LUMIX_FZ18.RAW
// - Panasonic/DMC-FZ70/P1000836.RW2
// - Samsung/NX200/2013-05-08-194524__sam6589.srw
// - Sony/DSC-HX95/DSC00018.ARW
//
// Some files are problematic and we won't bother with special-casing:
// - Leaf/Aptus 22/L_003172.mos (JPEG)'s thumbnail wrongly contains
// Exif Orientation 6, and sizes.flip also contains 6.
// - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color.
// - Ricoh/GXR/R0017428.DNG (JPEG) seems to be plainly invalid.
FivIoOrientation orientation = FivIoOrientationUnknown;
cairo_surface_t *surface = NULL;
#ifndef HAVE_LIBRAW
// In this case, g_mapped_file_get_contents() returns NULL, causing issues.
if (!g_mapped_file_get_length(mf)) {
set_error(error, "empty file");
return NULL;
}
FivIoImage *image = NULL;
#ifdef HAVE_LIBRAW
image = extract_libraw(target, mf, error);
#else // ! HAVE_LIBRAW
// TODO(p): Implement our own thumbnail extractors.
set_error(error, "unsupported file");
#else // HAVE_LIBRAW
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");
goto fail;
}
int err = 0;
if ((err = libraw_open_buffer(iprc, (void *) g_mapped_file_get_contents(mf),
g_mapped_file_get_length(mf))) ||
(err = libraw_unpack_thumb(iprc))) {
set_error(error, libraw_strerror(err));
goto fail_libraw;
}
libraw_processed_image_t *image = libraw_dcraw_make_mem_thumb(iprc, &err);
if (!image) {
set_error(error, libraw_strerror(err));
goto fail_libraw;
}
gboolean dummy = FALSE;
switch (image->type) {
case LIBRAW_IMAGE_JPEG:
surface = render(
target, g_bytes_new(image->data, image->data_size), &dummy, error);
orientation = (int) (intptr_t) cairo_surface_get_user_data(
surface, &fiv_io_key_orientation);
break;
case LIBRAW_IMAGE_BITMAP:
// Anything else is extremely rare.
if (image->colors != 3 || image->bits != 8) {
set_error(error, "unsupported bitmap thumbnail");
break;
}
surface = cairo_image_surface_create(
CAIRO_FORMAT_RGB24, image->width, image->height);
guint32 *out = (guint32 *) cairo_image_surface_get_data(surface);
const unsigned char *in = image->data;
for (guint64 i = 0; i < image->width * image->height; in += 3)
out[i++] = in[0] << 16 | in[1] << 8 | in[2];
cairo_surface_mark_dirty(surface);
// LibRaw actually turns an 8 to 5, so follow the documentation.
switch (iprc->sizes.flip) {
break; case 3: orientation = FivIoOrientation180;
break; case 5: orientation = FivIoOrientation270;
break; case 6: orientation = FivIoOrientation90;
}
break;
default:
set_error(error, "unsupported embedded thumbnail");
}
libraw_dcraw_clear_mem(image);
fail_libraw:
libraw_close(iprc);
#endif // HAVE_LIBRAW
fail:
#endif // ! HAVE_LIBRAW
g_mapped_file_unref(mf);
// This hardcodes Exif orientation before adjust_thumbnail() might do so,
// before the early return below.
surface = orient_thumbnail(surface, orientation);
if (!surface || max_size < FIV_THUMBNAIL_SIZE_MIN ||
max_size > FIV_THUMBNAIL_SIZE_MAX)
return surface;
if (!image)
return NULL;
if (max_size < FIV_THUMBNAIL_SIZE_MIN || max_size > FIV_THUMBNAIL_SIZE_MAX)
return fiv_io_image_to_surface(orient_thumbnail(image));
cairo_surface_t *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
cairo_surface_destroy(surface);
return result;
FivIoImage *result =
adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
fiv_io_image_unref(image);
return fiv_io_image_to_surface(result);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static WebPData
encode_thumbnail(cairo_surface_t *surface)
encode_thumbnail(FivIoImage *image)
{
WebPData bitstream = {};
WebPConfig config = {};
@@ -372,12 +570,12 @@ encode_thumbnail(cairo_surface_t *surface)
if (!WebPValidateConfig(&config))
return bitstream;
bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size);
bitstream.bytes = fiv_io_encode_webp(image, &config, &bitstream.size);
return bitstream;
}
static void
save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
save_thumbnail(FivIoImage *thumbnail, const char *path, GString *thum)
{
WebPMux *mux = WebPMuxNew();
WebPData bitstream = encode_thumbnail(thumbnail);
@@ -433,20 +631,21 @@ fiv_thumbnail_produce_for_search(
return NULL;
gboolean color_managed = FALSE;
cairo_surface_t *surface = render(target, data, &color_managed, error);
if (!surface)
FivIoImage *image = render(target, data, &color_managed, error);
if (!image)
return NULL;
// TODO(p): Might want to keep this a square.
cairo_surface_t *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
cairo_surface_destroy(surface);
return result;
FivIoImage *result =
adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
fiv_io_image_unref(image);
return fiv_io_image_to_surface(result);
}
static cairo_surface_t *
produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
{
// Note that this comes with a TOCTTOU problem.
goffset filesize = 0;
GFileInfo *info = g_file_query_info(target,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_SIZE,
@@ -468,14 +667,14 @@ produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
return NULL;
gboolean color_managed = FALSE;
cairo_surface_t *surface = render(target, data, &color_managed, error);
if (!surface)
FivIoImage *image = render(target, data, &color_managed, error);
if (!image)
return NULL;
cairo_surface_t *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[size].size);
cairo_surface_destroy(surface);
return result;
FivIoImage *result =
adjust_thumbnail(image, fiv_thumbnail_sizes[size].size);
fiv_io_image_unref(image);
return fiv_io_image_to_surface(result);
}
cairo_surface_t *
@@ -497,6 +696,13 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
return NULL;
}
// TODO(p): Use open(O_RDONLY | O_NONBLOCK | _O_BINARY), fstat(),
// g_mapped_file_new_from_fd(), and reset the non-blocking flag on the file.
if (!S_ISREG(st.st_mode)) {
set_error(error, "not a regular file");
return NULL;
}
GError *e = NULL;
GMappedFile *mf = g_mapped_file_new(path, FALSE, &e);
if (!mf) {
@@ -505,12 +711,18 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
return produce_fallback(target, max_size, error);
}
// In this case, g_mapped_file_get_bytes() has NULL data, causing issues.
gsize filesize = g_mapped_file_get_length(mf);
if (!filesize) {
set_error(error, "empty file");
return NULL;
}
gboolean color_managed = FALSE;
cairo_surface_t *surface =
FivIoImage *image =
render(target, g_mapped_file_get_bytes(mf), &color_managed, error);
g_mapped_file_unref(mf);
if (!surface)
if (!image)
return NULL;
// Boilerplate copied from fiv_thumbnail_lookup().
@@ -524,14 +736,12 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_string_append_printf(
thum, "%s%c%ld%c", THUMB_MTIME, 0, (long) st.st_mtime, 0);
g_string_append_printf(
thum, "%s%c%ld%c", THUMB_SIZE, 0, (long) filesize, 0);
thum, "%s%c%llu%c", THUMB_SIZE, 0, (unsigned long long) filesize, 0);
if (cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE) {
g_string_append_printf(thum, "%s%c%d%c", THUMB_IMAGE_WIDTH, 0,
cairo_image_surface_get_width(surface), 0);
g_string_append_printf(thum, "%s%c%d%c", THUMB_IMAGE_HEIGHT, 0,
cairo_image_surface_get_height(surface), 0);
}
g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_WIDTH, 0,
(unsigned) image->width, 0);
g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_HEIGHT, 0,
(unsigned) image->height, 0);
// Without a CMM, no conversion is attempted.
if (color_managed) {
@@ -539,19 +749,19 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
thum, "%s%c%s%c", THUMB_COLORSPACE, 0, THUMB_COLORSPACE_SRGB, 0);
}
cairo_surface_t *max_size_surface = NULL;
FivIoImage *max_size_image = NULL;
for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) {
cairo_surface_t *scaled =
adjust_thumbnail(surface, fiv_thumbnail_sizes[use].size);
FivIoImage *scaled =
adjust_thumbnail(image, fiv_thumbnail_sizes[use].size);
gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir,
fiv_thumbnail_sizes[use].thumbnail_spec_name, sum);
save_thumbnail(scaled, path, thum);
g_free(path);
if (!max_size_surface)
max_size_surface = scaled;
if (!max_size_image)
max_size_image = scaled;
else
cairo_surface_destroy(scaled);
fiv_io_image_unref(scaled);
}
g_string_free(thum, TRUE);
@@ -559,13 +769,20 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_free(thumbnails_dir);
g_free(sum);
g_free(uri);
cairo_surface_destroy(surface);
return max_size_surface;
fiv_io_image_unref(image);
return fiv_io_image_to_surface(max_size_image);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct {
const char *uri; ///< Target URI
time_t mtime; ///< File modification time
guint64 size; ///< File size
} Stat;
static bool
check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
bool *sRGB)
check_wide_thumbnail_texts(GBytes *thum, const Stat *st, bool *sRGB)
{
gsize len = 0;
const gchar *s = g_bytes_get_data(thum, &len), *end = s + len;
@@ -579,11 +796,14 @@ check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
continue;
} else if (!strcmp(key, THUMB_URI)) {
have_uri = true;
if (strcmp(target, s))
if (strcmp(st->uri, s))
return false;
} else if (!strcmp(key, THUMB_MTIME)) {
have_mtime = true;
if (atol(s) != mtime)
if (atol(s) != st->mtime)
return false;
} else if (!strcmp(key, THUMB_SIZE)) {
if (strtoull(s, NULL, 10) != st->size)
return false;
} else if (!strcmp(key, THUMB_COLORSPACE))
*sRGB = !strcmp(s, THUMB_COLORSPACE_SRGB);
@@ -594,30 +814,29 @@ check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
}
static cairo_surface_t *
read_wide_thumbnail(
const char *path, const char *uri, time_t mtime, GError **error)
read_wide_thumbnail(const char *path, const Stat *st, GError **error)
{
gchar *thumbnail_uri = g_filename_to_uri(path, NULL, error);
if (!thumbnail_uri)
return NULL;
cairo_surface_t *surface =
FivIoImage *image =
fiv_io_open(&(FivIoOpenContext){.uri = thumbnail_uri}, error);
g_free(thumbnail_uri);
if (!surface)
if (!image)
return NULL;
bool sRGB = false;
GBytes *thum = cairo_surface_get_user_data(surface, &fiv_io_key_thum);
if (!thum) {
if (!image->thum) {
g_clear_error(error);
set_error(error, "not a thumbnail");
} else if (!check_wide_thumbnail_texts(thum, uri, mtime, &sRGB)) {
} else if (!check_wide_thumbnail_texts(image->thum, st, &sRGB)) {
g_clear_error(error);
set_error(error, "mismatch");
} else {
// TODO(p): Add a function or a non-valueless define to check
// for CMM presence, then remove this ifdef.
cairo_surface_t *surface = fiv_io_image_to_surface(image);
#ifdef HAVE_LCMS2
if (!sRGB)
mark_thumbnail_lq(surface);
@@ -625,24 +844,21 @@ read_wide_thumbnail(
return surface;
}
cairo_surface_destroy(surface);
fiv_io_image_unref(image);
return NULL;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static cairo_surface_t *
read_png_thumbnail(
const char *path, const char *uri, time_t mtime, GError **error)
read_png_thumbnail(const char *path, const Stat *st, GError **error)
{
cairo_surface_t *surface = fiv_io_open_png_thumbnail(path, error);
if (!surface)
FivIoImage *image = fiv_io_open_png_thumbnail(path, error);
if (!image)
return NULL;
GHashTable *texts = cairo_surface_get_user_data(surface, &fiv_io_key_text);
GHashTable *texts = image->text;
if (!texts) {
set_error(error, "not a thumbnail");
cairo_surface_destroy(surface);
fiv_io_image_unref(image);
return NULL;
}
@@ -650,18 +866,27 @@ read_png_thumbnail(
// but those aren't interesting currently (would be for fast previews).
const char *text_uri = g_hash_table_lookup(texts, THUMB_URI);
const char *text_mtime = g_hash_table_lookup(texts, THUMB_MTIME);
if (!text_uri || strcmp(text_uri, uri) ||
!text_mtime || atol(text_mtime) != mtime) {
const char *text_size = g_hash_table_lookup(texts, THUMB_SIZE);
if (!text_uri || strcmp(text_uri, st->uri) ||
!text_mtime || atol(text_mtime) != st->mtime) {
set_error(error, "mismatch or not a thumbnail");
cairo_surface_destroy(surface);
fiv_io_image_unref(image);
return NULL;
}
if (text_size && strtoull(text_size, NULL, 10) != st->size) {
set_error(error, "file size mismatch");
fiv_io_image_unref(image);
return NULL;
}
return surface;
return fiv_io_image_to_surface(image);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cairo_surface_t *
fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, guint64 filesize,
FivThumbnailSize size)
{
g_return_val_if_fail(size >= FIV_THUMBNAIL_SIZE_MIN &&
size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
@@ -673,6 +898,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
gchar *thumbnails_dir = fiv_thumbnail_get_root();
const Stat st = {.uri = uri, .mtime = mtime_msec / 1000, .size = filesize};
// The lookup sequence is: nominal..max, then mirroring back to ..min.
cairo_surface_t *result = NULL;
@@ -685,7 +911,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
const char *name = fiv_thumbnail_sizes[use].thumbnail_spec_name;
gchar *wide = g_strconcat(thumbnails_dir, G_DIR_SEPARATOR_S "wide-",
name, G_DIR_SEPARATOR_S, sum, ".webp", NULL);
result = read_wide_thumbnail(wide, uri, mtime_msec / 1000, &error);
result = read_wide_thumbnail(wide, &st, &error);
if (error) {
g_debug("%s: %s", wide, error->message);
g_clear_error(&error);
@@ -701,7 +927,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
gchar *path = g_strconcat(thumbnails_dir, G_DIR_SEPARATOR_S,
name, G_DIR_SEPARATOR_S, sum, ".png", NULL);
result = read_png_thumbnail(path, uri, mtime_msec / 1000, &error);
result = read_png_thumbnail(path, &st, &error);
if (error) {
g_debug("%s: %s", path, error->message);
g_clear_error(&error);
@@ -734,7 +960,7 @@ print_error(GFile *file, GError *error)
}
static gchar *
identify_wide_thumbnail(GMappedFile *mf, time_t *mtime, GError **error)
identify_wide_thumbnail(GMappedFile *mf, Stat *st, GError **error)
{
WebPDemuxer *demux = WebPDemux(&(WebPData) {
.bytes = (const uint8_t *) g_mapped_file_get_contents(mf),
@@ -760,7 +986,9 @@ identify_wide_thumbnail(GMappedFile *mf, time_t *mtime, GError **error)
if (!strcmp(key, THUMB_URI) && !uri)
uri = g_strdup(p);
if (!strcmp(key, THUMB_MTIME))
*mtime = atol(p);
st->mtime = atol(p);
if (!strcmp(key, THUMB_SIZE))
st->size = strtoull(p, NULL, 10);
key = NULL;
} else {
key = p;
@@ -778,16 +1006,17 @@ static void
check_wide_thumbnail(GFile *thumbnail, GError **error)
{
// Not all errors are enough of a reason for us to delete something.
GError *tolerable = NULL;
GError *tolerable_error = NULL;
const char *path = g_file_peek_path(thumbnail);
GMappedFile *mf = g_mapped_file_new(path, FALSE, &tolerable);
GMappedFile *mf = g_mapped_file_new(path, FALSE, &tolerable_error);
if (!mf) {
print_error(thumbnail, tolerable);
print_error(thumbnail, tolerable_error);
return;
}
time_t target_mtime = 0;
gchar *target_uri = identify_wide_thumbnail(mf, &target_mtime, error);
// Note that we could enforce the presence of the size field in our spec.
Stat target_st = {.uri = NULL, .mtime = 0, .size = G_MAXUINT64};
gchar *target_uri = identify_wide_thumbnail(mf, &target_st, error);
g_mapped_file_unref(mf);
if (!target_uri)
return;
@@ -809,26 +1038,32 @@ check_wide_thumbnail(GFile *thumbnail, GError **error)
GFile *target = g_file_new_for_uri(target_uri);
g_free(target_uri);
GFileInfo *info = g_file_query_info(target,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
G_FILE_QUERY_INFO_NONE, NULL, &tolerable);
G_FILE_ATTRIBUTE_STANDARD_NAME ","
G_FILE_ATTRIBUTE_STANDARD_SIZE ","
G_FILE_ATTRIBUTE_TIME_MODIFIED,
G_FILE_QUERY_INFO_NONE, NULL, &tolerable_error);
g_object_unref(target);
if (g_error_matches(tolerable, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
g_propagate_error(error, tolerable);
if (g_error_matches(tolerable_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
g_propagate_error(error, tolerable_error);
return;
} else if (tolerable) {
print_error(thumbnail, tolerable);
} else if (tolerable_error) {
print_error(thumbnail, tolerable_error);
return;
}
guint64 filesize = g_file_info_get_size(info);
GDateTime *mdatetime = g_file_info_get_modification_date_time(info);
g_object_unref(info);
if (!mdatetime) {
set_error(&tolerable, "cannot retrieve file modification time");
print_error(thumbnail, tolerable);
set_error(&tolerable_error, "cannot retrieve file modification time");
print_error(thumbnail, tolerable_error);
return;
}
if (g_date_time_to_unix(mdatetime) != target_mtime)
set_error(error, "mtime mismatch");
if (g_date_time_to_unix(mdatetime) != target_st.mtime)
set_error(error, "modification time mismatch");
else if (target_st.size != G_MAXUINT64 && filesize != target_st.size)
set_error(error, "file size mismatch");
g_date_time_unref(mdatetime);
}

View File

@@ -21,7 +21,7 @@
#include <gio/gio.h>
#include <glib.h>
// And this is how you avoid glib-mkenums.
// Avoid glib-mkenums.
typedef enum _FivThumbnailSize {
#define FIV_THUMBNAIL_SIZES(XX) \
XX(SMALL, 128, "normal") \
@@ -68,8 +68,8 @@ cairo_surface_t *fiv_thumbnail_produce_for_search(
/// 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, FivThumbnailSize size);
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);

View File

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

978
fiv.c

File diff suppressed because it is too large Load Diff

View File

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

71
fiv.wxs.in Normal file
View File

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

View File

@@ -1,7 +1,8 @@
# vim: noet ts=4 sts=4 sw=4:
project('fiv', 'c',
default_options : ['c_std=gnu99', 'warning_level=2'],
version : '0.1.0')
version : '1.0.0',
meson_version : '>=0.57')
cc = meson.get_compiler('c')
add_project_arguments(
@@ -25,16 +26,19 @@ libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
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.
# 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('libwebp'),
dependency('libwebpdemux'),
@@ -53,6 +57,24 @@ dependencies = [
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
@@ -73,11 +95,13 @@ endif
# XXX: https://github.com/mesonbuild/meson/issues/825
docdir = get_option('datadir') / 'doc' / meson.project_name()
application_ns = 'name.janouch.'
application_url = 'https://janouch.name/p/' + meson.project_name()
conf = configuration_data()
conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
conf.set_quoted('PROJECT_NS', application_ns)
conf.set_quoted('PROJECT_URL', application_url)
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
if win32
conf.set_quoted('PROJECT_DOCDIR', docdir)
@@ -85,6 +109,7 @@ endif
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())
@@ -118,7 +143,8 @@ if win32
'--width', size, '--height', size, '@INPUT@'])
endforeach
icon_ico = custom_target(input : icon_png_list, output : 'fiv.ico',
icon_ico = custom_target('fiv.ico',
output : 'fiv.ico', input : icon_png_list,
command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
rc += windows.compile_resources('fiv.rc', depends : icon_ico)
endif
@@ -133,23 +159,23 @@ gresources = gnome.compile_resources('resources',
tiff_tables = custom_target('tiff-tables.h',
output : 'tiff-tables.h',
input : 'tiff-tables.db',
command : ['tiff-tables.awk', '@INPUT@'],
# Meson 0.56 chokes on files() as well as on a relative path.
command : [meson.current_source_dir() / 'tiff-tables.awk', '@INPUT@'],
capture : true,
)
desktops = ['fiv.desktop', 'fiv-browse.desktop']
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-io.c', 'fiv-context-menu.c',
iolib = static_library('fiv-io', 'fiv-io.c', 'fiv-io-cmm.c', 'xdg.c',
tiff_tables, config,
dependencies : dependencies).extract_all_objects(recursive : true)
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
'xdg.c', gresources, rc, config,
install : true,
'fiv-io-model.c', gresources, rc, config,
objects : iolib,
dependencies : dependencies,
install : true,
win_subsystem : 'windows',
)
if gdkpixbuf.found()
executable('io-benchmark', 'fiv-io-benchmark.c', 'fiv-io.c', 'xdg.c',
build_by_default : false,
dependencies : [dependencies, gdkpixbuf])
endif
desktops += 'fiv-jpegcrop.desktop'
jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
@@ -162,38 +188,55 @@ jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
)
if get_option('tools').enabled()
# libjq 1.6 lacks a pkg-config file, and there is no release in sight.
# libjq 1.6 is required.
tools_dependencies = [cc.find_library('jq'), dependency('libpng')]
# 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 : ['pnginfo', 'jpeginfo', 'tiffinfo', 'webpinfo', 'bmffinfo']
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
install_data(schema,
rename : [application_ns + schema],
configure_file(
input : schema,
output : application_ns + schema,
copy : true,
install: true,
install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
endforeach
# For the purposes of development: make the program find its GSettings schemas.
gnome.compile_schemas(depend_files : files(gsettings_schemas))
gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
# Validate various files, if there are tools around to do it.
xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
gsettings_schemas
xmls += run_command(find_program('sed', required : false, disabler : true),
'-n', 's@.*>\([^<>]*[.]svg\)<.*@resources/\\1@p',
# Meson is broken on Windows and removes the backslashes, so this ends up empty.
symbolics = run_command(find_program('sed', required : false, disabler : true),
'-n', 's@.*>\\([^<>]*[.]svg\\)<.*@resources/\\1@p',
configure_file(
input : 'resources/resources.gresource.xml',
output : 'resources.gresource.xml.stamp',
copy : true,
), capture : true, check : true).stdout().strip().split('\n')
), capture : true, check : true).stdout().strip()
# Validate various files, if there are tools around to do it.
xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
gsettings_schemas
if symbolics != ''
xmls += symbolics.split('\n')
endif
xmlwf = find_program('xmlwf', required : false, disabler : true)
xmllint = find_program('xmllint', required : false, disabler : true)
@@ -233,7 +276,8 @@ if not win32
else
command = ['env', 'LC_ALL=C',
'asciidoc-release-version=' + meson.project_version(),
'awk', '-f', files('liberty/tools/asciiman.awk'), '@INPUT@']
'awk', '-f', files('submodules/liberty/tools/asciiman.awk'),
'@INPUT@']
man_capture = true
endif
custom_target('manpage for ' + page,
@@ -295,11 +339,44 @@ if not win32
if not meson.is_cross_build()
meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
endif
elif meson.is_cross_build()
msys2_root = meson.get_external_property('msys2_root')
meson.add_install_script('msys2-cross-install.sh', msys2_root)
# This is the minimum to run targets from msys2-cross-configure.sh builds.
# Quick and dirty package generation, lacking dependencies.
packaging = configuration_data({
'name' : meson.project_name(),
'version' : meson.project_version(),
'summary' : 'Image viewer',
'author' : 'Přemysl Eric Janouch',
})
subdir('submodules/liberty/meson/packaging')
elif meson.is_cross_build()
# Note that even compiling /from within MSYS2/ can still be a cross-build.
msys2_root = meson.get_external_property('msys2_root')
meson.add_install_script('msys2-install.sh', msys2_root)
wxs = configure_file(
input : 'fiv.wxs.in',
output : 'fiv.wxs',
configuration : configuration_data({
'ProjectName' : meson.project_name(),
'ProjectVersion' : meson.project_version(),
'ProjectURL' : application_url,
}),
)
msi = meson.project_name() + '-' + meson.project_version() + \
'-' + host_machine.cpu() + '.msi'
custom_target('package',
output : msi,
command : [meson.current_source_dir() / 'msys2-package.sh',
host_machine.cpu(), msi, wxs],
env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
console : true,
build_always_stale : true,
build_by_default : false,
)
# This is the minimum to run targets from msys2-configure.sh builds.
meson.add_devenv({
'WINEPATH' : msys2_root / 'bin',
'XDG_DATA_DIRS' : msys2_root / 'share',

View File

@@ -3,6 +3,8 @@ option('tools', type : 'feature', value : 'disabled',
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',

View File

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

View File

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

35
msys2-package.sh Executable file
View File

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

View File

@@ -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

@@ -6,6 +6,7 @@
<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>

View File

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

View File

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

View File

@@ -2,6 +2,22 @@
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"
}
{
@@ -55,8 +71,10 @@ function flushvalues() {
function flushsection() {
if (section) {
flushvalues()
print "};\n\n" allvalues "static struct tiff_entry " \
print "};\n\n" allvalues "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
print "static struct tiff_entry " \
sectionsnakecase "_entries[] = {" fields "\n\t{}\n};"
print "#endif"
}
}

View File

@@ -30,8 +30,14 @@
# 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
@@ -64,6 +70,8 @@
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
@@ -185,6 +193,9 @@
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
@@ -192,7 +203,18 @@
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 # ISO 12234 TIFF/EP image data format
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
@@ -425,3 +447,25 @@
# 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;
}

View File

@@ -1,7 +1,7 @@
//
// fiv-io-benchmark.c: see if we suck
// benchmark-io.c: measure and compare image loading times
//
// Copyright (c) 2021 - 2022, 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.
@@ -32,35 +32,36 @@ timestamp(void)
static void
one_file(const char *filename)
{
double since_us = timestamp();
GFile *file = g_file_new_for_commandline_arg(filename);
double since_us = timestamp(), us = 0;
FivIoOpenContext ctx = {
.uri = g_filename_to_uri(filename, NULL, NULL),
.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),
};
cairo_surface_t *loaded_by_us = fiv_io_open(&ctx, NULL);
FivIoImage *loaded_by_us = fiv_io_open(&ctx, NULL);
g_clear_object(&file);
g_free((char *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE);
if (!loaded_by_us)
return;
cairo_surface_destroy(loaded_by_us);
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,142 +0,0 @@
//
// bmffinfo.c: acquire information about BMFF files in JSON format
//
// 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 "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 jv
parse_bmff(jv o, 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.
if (len < 8 || memcmp(p + 4, "ftyp", 4))
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;
}
// --- 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_bmff(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[])
{
(void) parse_icc;
(void) parse_exif;
(void) parse_psir;
// 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++) {
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;
}

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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,610 +0,0 @@
//
// jpeginfo.c: acquire information about JPEG files in JSON format
//
// 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 "info.h"
#include <jv.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- Multi-Picture Format ----------------------------------------------------
enum {
MPF_MPFVersion = 45056,
MPF_NumberOfImages = 45057,
MPF_MPEntry = 45058,
MPF_ImageUIDList = 45059,
MPF_TotalFrames = 45060,
MPF_MPIndividualNum = 45313,
MPF_PanOrientation = 45569,
MPF_PanOverlap_H = 45570,
MPF_PanOverlap_V = 45571,
MPF_BaseViewpointNum = 45572,
MPF_ConvergenceAngle = 45573,
MPF_BaselineLength = 45574,
MPF_VerticalDivergence = 45575,
MPF_AxisDistance_X = 45576,
MPF_AxisDistance_Y = 45577,
MPF_AxisDistance_Z = 45578,
MPF_YawAngle = 45579,
MPF_PitchAngle = 45580,
MPF_RollAngle = 45581
};
static struct tiff_entry mpf_entries[] = {
{"MP Format Version Number", MPF_MPFVersion, NULL},
{"Number of Images", MPF_NumberOfImages, NULL},
{"MP Entry", MPF_MPEntry, NULL},
{"Individual Image Unique ID List", MPF_ImageUIDList, NULL},
{"Total Number of Captured Frames", MPF_TotalFrames, NULL},
{"MP Individual Image Number", MPF_MPIndividualNum, NULL},
{"Panorama Scanning Orientation", MPF_PanOrientation, NULL},
{"Panorama Horizontal Overlap", MPF_PanOverlap_H, NULL},
{"Panorama Vertical Overlap", MPF_PanOverlap_V, NULL},
{"Base Viewpoint Number", MPF_BaseViewpointNum, NULL},
{"Convergence Angle", MPF_ConvergenceAngle, NULL},
{"Baseline Length", MPF_BaselineLength, NULL},
{"Divergence Angle", MPF_VerticalDivergence, NULL},
{"Horizontal Axis Distance", MPF_AxisDistance_X, NULL},
{"Vertical Axis Distance", MPF_AxisDistance_Y, NULL},
{"Collimation Axis Distance", MPF_AxisDistance_Z, NULL},
{"Yaw Angle", MPF_YawAngle, NULL},
{"Pitch Angle", MPF_PitchAngle, NULL},
{"Roll Angle", MPF_RollAngle, NULL},
{}
};
static uint32_t
parse_mpf_mpentry(jv *a, const uint8_t *p, struct tiffer *T)
{
uint32_t attrs = T->un->u32(p);
uint32_t offset = T->un->u32(p + 8);
uint32_t type_number = attrs & 0xFFFFFF;
jv type = jv_number(type_number);
switch (type_number) {
break; case 0x030000: type = jv_string("Baseline MP Primary Image");
break; case 0x010001: type = jv_string("Large Thumbnail - VGA");
break; case 0x010002: type = jv_string("Large Thumbnail - Full HD");
break; case 0x020001: type = jv_string("Multi-Frame Image Panorama");
break; case 0x020002: type = jv_string("Multi-Frame Image Disparity");
break; case 0x020003: type = jv_string("Multi-Frame Image Multi-Angle");
break; case 0x000000: type = jv_string("Undefined");
}
uint32_t format_number = (attrs >> 24) & 0x7;
jv format = jv_number(format_number);
if (format_number == 0)
format = jv_string("JPEG");
*a = jv_array_append(*a, JV_OBJECT(
jv_string("Individual Image Attribute"), JV_OBJECT(
jv_string("Dependent Parent Image"), jv_bool((attrs >> 31) & 1),
jv_string("Dependent Child Image"), jv_bool((attrs >> 30) & 1),
jv_string("Representative Image"), jv_bool((attrs >> 29) & 1),
jv_string("Reserved"), jv_number((attrs >> 27) & 0x3),
jv_string("Image Data Format"), format,
jv_string("MP Type Code"), type
),
jv_string("Individual Image Size"),
jv_number(T->un->u32(p + 4)),
jv_string("Individual Image Data Offset"),
jv_number(offset),
jv_string("Dependent Image 1 Entry Number"),
jv_number(T->un->u16(p + 12)),
jv_string("Dependent Image 2 Entry Number"),
jv_number(T->un->u16(p + 14))
));
// Don't report non-JPEGs, even though they're unlikely.
return format_number == 0 ? offset : 0;
}
static jv
parse_mpf_index_entry(jv o, const uint8_t ***offsets, struct tiffer *T,
struct tiffer_entry *entry)
{
// 5.2.3.3. MP Entry
if (entry->tag != MPF_MPEntry || entry->type != UNDEFINED ||
entry->remaining_count % 16) {
return parse_exif_entry(o, T, entry, mpf_entries);
}
uint32_t count = entry->remaining_count / 16;
jv a = jv_array_sized(count);
const uint8_t **out = *offsets = calloc(sizeof *out, count + 1);
for (uint32_t i = 0; i < count; i++) {
uint32_t offset = parse_mpf_mpentry(&a, entry->p + i * 16, T);
if (offset)
*out++ = T->begin + offset;
}
return jv_set(o, jv_string("MP Entry"), a);
}
static jv
parse_mpf_index_ifd(const uint8_t ***offsets, struct tiffer *T)
{
jv ifd = jv_object();
struct tiffer_entry entry = {};
while (tiffer_next_entry(T, &entry))
ifd = parse_mpf_index_entry(ifd, offsets, T, &entry);
return ifd;
}
static jv
parse_mpf(jv o, const uint8_t ***offsets, const uint8_t *p, size_t len)
{
struct tiffer T;
if (!tiffer_init(&T, p, len) || !tiffer_next_ifd(&T))
return add_warning(o, "invalid MPF segment");
// First image: IFD0 is Index IFD, any IFD1 is Attribute IFD.
// Other images: IFD0 is Attribute IFD, there is no Index IFD.
if (!*offsets) {
o = add_to_subarray(o, "MPF", parse_mpf_index_ifd(offsets, &T));
if (!tiffer_next_ifd(&T))
return o;
}
// This isn't optimal, but it will do.
return add_to_subarray(o, "MPF", parse_exif_ifd(&T, mpf_entries));
}
// --- JPEG --------------------------------------------------------------------
// Because the JPEG file format is simple, just do it manually.
// See: https://www.w3.org/Graphics/JPEG/itu-t81.pdf
enum {
TEM = 0x01,
SOF0 = 0xC0, SOF1, SOF2, SOF3,
DHT = 0xC4,
SOF5, SOF6, SOF7,
JPG = 0xC8,
SOF9, SOF10, SOF11,
DAC = 0xCC,
SOF13, SOF14, SOF15,
RST0 = 0xD0, RST1, RST2, RST3, RST4, RST5, RST6, RST7,
SOI = 0xD8,
EOI = 0xD9,
SOS = 0xDA,
DQT = 0xDB,
DNL = 0xDC,
DRI = 0xDD,
DHP = 0xDE,
EXP = 0xDF,
APP0 = 0xE0, APP1, APP2, APP3, APP4, APP5, APP6, APP7,
APP8, APP9, APP10, APP11, APP12, APP13, APP14, APP15,
JPG0 = 0xF0, JPG1, JPG2, JPG3, JPG4, JPG5, JPG6, JPG7,
JPG8, JPG9, JPG10, JPG11, JPG12, JPG13,
COM = 0xFE
};
// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
static const char *marker_ids[0xFF] = {
[TEM] = "TEM",
[SOF0] = "SOF0", [SOF1] = "SOF1", [SOF2] = "SOF2", [SOF3] = "SOF3",
[DHT] = "DHT", [SOF5] = "SOF5", [SOF6] = "SOF6", [SOF7] = "SOF7",
[JPG] = "JPG", [SOF9] = "SOF9", [SOF10] = "SOF10", [SOF11] = "SOF11",
[DAC] = "DAC", [SOF13] = "SOF13", [SOF14] = "SOF14", [SOF15] = "SOF15",
[RST0] = "RST0", [RST1] = "RST1", [RST2] = "RST2", [RST3] = "RST3",
[RST4] = "RST4", [RST5] = "RST5", [RST6] = "RST6", [RST7] = "RST7",
[SOI] = "SOI", [EOI] = "EOI", [SOS] = "SOS", [DQT] = "DQT",
[DNL] = "DNL", [DRI] = "DRI", [DHP] = "DHP", [EXP] = "EXP",
[APP0] = "APP0", [APP1] = "APP1", [APP2] = "APP2", [APP3] = "APP3",
[APP4] = "APP4", [APP5] = "APP5", [APP6] = "APP6", [APP7] = "APP7",
[APP8] = "APP8", [APP9] = "APP9", [APP10] = "APP10", [APP11] = "APP11",
[APP12] = "APP12", [APP13] = "APP13", [APP14] = "APP14", [APP15] = "APP15",
[JPG0] = "JPG0", [JPG1] = "JPG1", [JPG2] = "JPG2", [JPG3] = "JPG3",
[JPG4] = "JPG4", [JPG5] = "JPG5", [JPG6] = "JPG6", [JPG7] = "JPG7",
[JPG8] = "JPG8", [JPG9] = "JPG9", [JPG10] = "JPG10", [JPG11] = "JPG11",
[JPG12] = "JPG12", [JPG13] = "JPG13", [COM] = "COM"
};
// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
static const char *marker_descriptions[0xFF] = {
[TEM] = "For temporary private use in arithmetic coding",
[SOF0] = "Baseline DCT",
[SOF1] = "Extended sequential DCT",
[SOF2] = "Progressive DCT",
[SOF3] = "Lossless (sequential)",
[DHT] = "Define Huffman table(s)",
[SOF5] = "Differential sequential DCT",
[SOF6] = "Differential progressive DCT",
[SOF7] = "Differential lossless (sequential)",
[JPG] = "Reserved for JPEG extensions",
[SOF9] = "Extended sequential DCT",
[SOF10] = "Progressive DCT",
[SOF11] = "Lossless (sequential)",
[DAC] = "Define arithmetic coding conditioning(s)",
[SOF13] = "Differential sequential DCT",
[SOF14] = "Differential progressive DCT",
[SOF15] = "Differential lossless (sequential)",
[RST0] = "Restart with module 8 count 0",
[RST1] = "Restart with module 8 count 1",
[RST2] = "Restart with module 8 count 2",
[RST3] = "Restart with module 8 count 3",
[RST4] = "Restart with module 8 count 4",
[RST5] = "Restart with module 8 count 5",
[RST6] = "Restart with module 8 count 6",
[RST7] = "Restart with module 8 count 7",
[SOI] = "Start of image",
[EOI] = "End of image",
[SOS] = "Start of scan",
[DQT] = "Define quantization table(s)",
[DNL] = "Define number of lines",
[DRI] = "Define restart interval",
[DHP] = "Define hierarchical progression",
[EXP] = "Expand reference component(s)",
[APP0] = "Reserved for application segments, 0",
[APP1] = "Reserved for application segments, 1",
[APP2] = "Reserved for application segments, 2",
[APP3] = "Reserved for application segments, 3",
[APP4] = "Reserved for application segments, 4",
[APP5] = "Reserved for application segments, 5",
[APP6] = "Reserved for application segments, 6",
[APP7] = "Reserved for application segments, 7",
[APP8] = "Reserved for application segments, 8",
[APP9] = "Reserved for application segments, 9",
[APP10] = "Reserved for application segments, 10",
[APP11] = "Reserved for application segments, 11",
[APP12] = "Reserved for application segments, 12",
[APP13] = "Reserved for application segments, 13",
[APP14] = "Reserved for application segments, 14",
[APP15] = "Reserved for application segments, 15",
[JPG0] = "Reserved for JPEG extensions, 0",
[JPG1] = "Reserved for JPEG extensions, 1",
[JPG2] = "Reserved for JPEG extensions, 2",
[JPG3] = "Reserved for JPEG extensions, 3",
[JPG4] = "Reserved for JPEG extensions, 4",
[JPG5] = "Reserved for JPEG extensions, 5",
[JPG6] = "Reserved for JPEG extensions, 6",
[JPG7] = "Reserved for JPEG extensions, 7",
[JPG8] = "Reserved for JPEG extensions, 8",
[JPG9] = "Reserved for JPEG extensions, 9",
[JPG10] = "Reserved for JPEG extensions, 10",
[JPG11] = "Reserved for JPEG extensions, 11",
[JPG12] = "Reserved for JPEG extensions, 12",
[JPG13] = "Reserved for JPEG extensions, 13",
[COM] = "Comment",
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct data {
bool ended;
uint8_t *exif, *icc, *psir;
size_t exif_len, icc_len, psir_len;
int icc_sequence, icc_done;
const uint8_t **mpf_offsets, **mpf_next;
};
static void
parse_append(uint8_t **buffer, size_t *buffer_len, const uint8_t *p, size_t len)
{
size_t buffer_longer = *buffer_len + len;
*buffer = realloc(*buffer, buffer_longer);
memcpy(*buffer + *buffer_len, p, len);
*buffer_len = buffer_longer;
}
static const uint8_t *
parse_marker(uint8_t marker, const uint8_t *p, const uint8_t *end,
struct data *data, jv *o)
{
// Suspected: MJPEG? Undetected format recursion, e.g., thumbnails?
// Found: Random metadata! Multi-Picture Format!
if ((data->ended = marker == EOI)) {
// TODO(p): Handle Exifs independently--flush the last one.
if ((data->mpf_next || (data->mpf_next = data->mpf_offsets)) &&
*data->mpf_next)
return *data->mpf_next++;
if (p != end)
*o = add_warning(*o, "trailing data");
}
// These markers stand alone, not starting a marker segment.
switch (marker) {
case RST0:
case RST1:
case RST2:
case RST3:
case RST4:
case RST5:
case RST6:
case RST7:
*o = add_warning(*o, "unexpected restart marker");
// Fall-through
case SOI:
case EOI:
case TEM:
return p;
}
uint16_t length = p[0] << 8 | p[1];
const uint8_t *payload = p + 2;
if ((p += length) > end) {
*o = add_error(*o, "runaway marker segment");
return NULL;
}
switch (marker) {
case SOF0:
case SOF1:
case SOF2:
case SOF3:
case SOF5:
case SOF6:
case SOF7:
case SOF9:
case SOF10:
case SOF11:
case SOF13:
case SOF14:
case SOF15:
case DHP: // B.2.2 and B.3.2.
// As per B.2.5, Y can be zero, then there needs to be a DNL segment.
*o = add_to_subarray(*o, "info", JV_OBJECT(
jv_string("type"), jv_string(marker_descriptions[marker]),
jv_string("bits"), jv_number(payload[0]),
jv_string("height"), jv_number(payload[1] << 8 | payload[2]),
jv_string("width"), jv_number(payload[3] << 8 | payload[4]),
jv_string("components"), jv_number(payload[5])
));
return p;
}
// See B.1.1.5, we can brute-force our way through the entropy-coded data.
if (marker == SOS) {
while (p + 2 <= end && (p[0] != 0xFF || p[1] < 0xC0 || p[1] > 0xFE ||
(p[1] >= RST0 && p[1] <= RST7)))
p++;
return p;
}
// "The interpretation is left to the application."
if (marker == COM) {
int superascii = 0;
char *buf = calloc(3, p - payload), *bufp = buf;
for (const uint8_t *q = payload; q < p; q++) {
if (*q < 128) {
*bufp++ = *q;
} else {
superascii++;
*bufp++ = 0xC0 | (*q >> 6);
*bufp++ = 0x80 | (*q & 0x3F);
}
}
*bufp++ = 0;
*o = add_to_subarray(*o, "comments", jv_string(buf));
free(buf);
if (superascii)
*o = add_warning(*o, "super-ASCII comments");
}
// These mostly contain an ASCII string header, following JPEG FIF:
//
// "Application-specific APP0 marker segments are identified
// by a zero terminated string which identifies the application
// (not 'JFIF' or 'JFXX')."
if (marker >= APP0 && marker <= APP15) {
const uint8_t *nul = memchr(payload, 0, p - payload);
int unprintable = !nul;
if (nul) {
for (const uint8_t *q = payload; q < nul; q++)
unprintable += *q < 32 || *q >= 127;
}
*o = add_to_subarray(*o, "apps",
unprintable ? jv_null() : jv_string((const char *) payload));
}
// CIPA DC-007 (Multi-Picture Format) 5.2
// http://fileformats.archiveteam.org/wiki/Multi-Picture_Format
if (marker == APP2 && p - payload >= 8 && !memcmp(payload, "MPF\0", 4)) {
payload += 4;
*o = parse_mpf(*o, &data->mpf_offsets, payload, p - payload);
}
// CIPA DC-006 (Stereo Still Image Format for Digital Cameras)
// TODO(p): Handle by properly skipping trailing data (use Stim offsets).
// https://www.w3.org/Graphics/JPEG/jfif3.pdf
if (marker == APP0 && p - payload >= 14 && !memcmp(payload, "JFIF\0", 5)) {
payload += 5;
jv units = jv_number(payload[2]);
switch (payload[2]) {
break; case 0: units = jv_null();
break; case 1: units = jv_string("DPI");
break; case 2: units = jv_string("dots per cm");
}
// The rest is picture data.
*o = add_to_subarray(*o, "JFIF", JV_OBJECT(
jv_string("version"), jv_number(payload[0] * 100 + payload[1]),
jv_string("units"), units,
jv_string("density-x"), jv_number(payload[3] << 8 | payload[4]),
jv_string("density-y"), jv_number(payload[5] << 8 | payload[6]),
jv_string("thumbnail-w"), jv_number(payload[7]),
jv_string("thumbnail-h"), jv_number(payload[8])
));
}
if (marker == APP0 && p - payload >= 6 && !memcmp(payload, "JFXX\0", 5)) {
payload += 5;
jv extension = jv_number(payload[0]);
switch (payload[0]) {
break; case 0x10: extension = jv_string("JPEG thumbnail");
break; case 0x11: extension = jv_string("Paletted thumbnail");
break; case 0x13: extension = jv_string("RGB thumbnail");
}
// The rest is picture data.
*o = add_to_subarray(*o, "JFXX",
JV_OBJECT(jv_string("extension"), extension));
}
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf 4.7.2
// Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3
if (marker == APP1 && p - payload >= 6 && !memcmp(payload, "Exif\0", 5)) {
payload += 6;
if (payload[-1] != 0)
*o = add_warning(*o, "weirdly padded Exif header");
if (data->exif)
*o = add_warning(*o, "multiple Exif segments");
parse_append(&data->exif, &data->exif_len, payload, p - payload);
}
// https://www.color.org/specification/ICC1v43_2010-12.pdf B.4
if (marker == APP2 && p - payload >= 14 &&
!memcmp(payload, "ICC_PROFILE\0", 12) && !data->icc_done &&
payload[12] == ++data->icc_sequence && payload[13] >= payload[12]) {
payload += 14;
parse_append(&data->icc, &data->icc_len, payload, p - payload);
data->icc_done = payload[-1] == data->icc_sequence;
}
// Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + 3.1.3
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
if (marker == APP13 && p - payload >= 14 &&
!memcmp(payload, "Photoshop 3.0\0", 14)) {
payload += 14;
parse_append(&data->psir, &data->psir_len, payload, p - payload);
}
// TODO(p): Extract all XMP segments.
return p;
}
static jv
parse_jpeg(jv o, const uint8_t *p, size_t len)
{
struct data data = {};
const uint8_t *end = p + len;
jv markers = jv_array();
while (p) {
// This is an expectable condition, use a simple warning.
if (p + 2 > end) {
if (!data.ended)
o = add_warning(o, "unexpected EOF");
break;
}
if (*p++ != 0xFF || *p == 0) {
if (!data.ended)
o = add_error(o, "no marker found where one was expected");
break;
}
// Markers may be preceded by fill bytes.
if (*p == 0xFF) {
o = jv_object_set(o, jv_string("fillers"), jv_bool(true));
continue;
}
uint8_t marker = *p++;
markers = jv_array_append(markers,
jv_string(marker_ids[marker] ? marker_ids[marker] : "RES"));
p = parse_marker(marker, p, end, &data, &o);
}
if (data.exif) {
o = parse_exif(o, data.exif, data.exif_len);
free(data.exif);
}
if (data.icc) {
if (data.icc_done)
o = parse_icc(o, data.icc, data.icc_len);
else
o = add_warning(o, "bad ICC profile sequence");
free(data.icc);
}
if (data.psir) {
o = parse_psir(o, data.psir, data.psir_len);
free(data.psir);
}
free(data.mpf_offsets);
return jv_set(o, jv_string("markers"), markers);
}
// --- 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;
}
#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_jpeg(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 . -iname *.png -print0 | xargs -0 ./pnginfo
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;
}

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;
}

View File

@@ -1,79 +0,0 @@
//
// tiffinfo.c: acquire information about TIFF files in JSON format
//
// 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 "info.h"
#include <jv.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// This is essentially the same as jpeginfo.c, but we only have an Exif segment.
// TODO(p): Photoshop data and ICC profiles also have their tag,
// they're not currently processed.
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_exif(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 . -iname *.png -print0 | xargs -0 ./pnginfo
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;
}

View File

@@ -1,133 +0,0 @@
//
// webpinfo.c: acquire information about WebP files in JSON format
//
// 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 "info.h"
#include <jv.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- WebP --------------------------------------------------------------------
// 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 jv
parse_webp(jv o, const uint8_t *p, size_t len)
{
// libwebp won't let us simply iterate over all chunks, so handroll it.
if (len < 12 || memcmp(p, "RIFF", 4) || memcmp(p + 8, "WEBP", 4))
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 VP8 and VP8L chunk metadata.
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 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_webp(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[])
{
(void) parse_psir;
// 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++) {
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;
}

3
xdg.c
View File

@@ -17,6 +17,9 @@
#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