Compare commits
269 Commits
04ec292caf
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
0fceaf7728
|
|||
|
c46fc73c34
|
|||
|
bdd18fc898
|
|||
|
cf6ded1d03
|
|||
|
3bea18708f
|
|||
|
ed8ba147ba
|
|||
|
c221a00c33
|
|||
|
192ffa0de9
|
|||
|
bac9fce4e0
|
|||
|
2e9ea9b4e2
|
|||
|
b34fe63198
|
|||
|
3c8ddcaf26
|
|||
|
e3ec07a19f
|
|||
|
e57364cd97
|
|||
|
7330f07dd7
|
|||
|
d68e09525c
|
|||
|
115a7bab0f
|
|||
|
91538aaba5
|
|||
|
c214e668d9
|
|||
|
a5ebc697ad
|
|||
|
9ca18f52d5
|
|||
|
604594a8f1
|
|||
|
9acab00bcc
|
|||
|
ae8dc3070a
|
|||
|
3c8a280546
|
|||
|
96189b70b8
|
|||
|
67433f3776
|
|||
|
c1418c7462
|
|||
|
935506b120
|
|||
|
84269b2ba2
|
|||
|
51ca3f8e2e
|
|||
|
f196b03e97
|
|||
|
ee08565389
|
|||
|
c04c4063e4
|
|||
|
aed6ae6b83
|
|||
|
bae640a116
|
|||
|
52c17c8a16
|
|||
|
b07fba0c9c
|
|||
|
72bf913f3d
|
|||
|
e79574fd56
|
|||
|
93ad75eb35
|
|||
|
2d10aa8b61
|
|||
|
1ec41f7749
|
|||
|
d4b91d6260
|
|||
|
5ec5f5bdbd
|
|||
|
840e7f172c
|
|||
|
9b99de99bb
|
|||
|
ab75d2b61d
|
|||
|
92deba3890
|
|||
|
668c5eb78a
|
|||
|
d713d5820c
|
|||
|
f05e66bfc1
|
|||
|
6ee5f69bfe
|
|||
|
4249898497
|
|||
|
117422ade5
|
|||
|
8ff33e6b63
|
|||
|
ce4a13ed38
|
|||
|
6a1b851130
|
|||
|
68245b55c9
|
|||
|
2869c656c1
|
|||
|
ec713b633e
|
|||
|
88234f8283
|
|||
|
49ee551b9b
|
|||
|
089c90004b
|
|||
|
19913a5e48
|
|||
|
1ef0a84bc7
|
|||
|
4b5b8ec9fa
|
|||
|
3449ac5a12
|
|||
|
bbfa2344d6
|
|||
|
2ff853b7e0
|
|||
|
bb4d3acd12
|
|||
|
074bd4d37f
|
|||
|
add96b37a6
|
|||
|
c2e8b65d0f
|
|||
|
4f57070e27
|
|||
|
2dc4e9c13b
|
|||
|
a1f6ffd226
|
|||
|
1eee1831a5
|
|||
|
86622e0c31
|
|||
|
a4772ce319
|
|||
|
0318424540
|
|||
|
8d5885bfdf
|
|||
|
41b5ddc744
|
|||
|
b308b5da18
|
|||
|
1577961aa2
|
|||
|
1fb42e689f
|
|||
|
8953e6beea
|
|||
|
2e8bbf0e43
|
|||
|
07d4ea2dde
|
|||
|
a5b5e32c3b
|
|||
|
1e8fe1411b
|
|||
|
274c5f6f66
|
|||
|
de377d3eae
|
|||
|
34388b93ea
|
|||
|
7dda3bd1ed
|
|||
|
a3a5eb33cf
|
|||
|
ee202ca28b
|
|||
|
04db6ed6a1
|
|||
|
d6e79cf976
|
|||
|
6cc4ca1f44
|
|||
|
1c25cb411f
|
|||
|
399c4bdf69
|
|||
|
a9b34ca3f2
|
|||
|
bd92ad73ec
|
|||
|
b3bc481172
|
|||
|
a3745df84b
|
|||
|
cc59e537da
|
|||
|
338ae69121
|
|||
|
1c61fcc5bc
|
|||
|
dd1d6647dc
|
|||
|
abf4f1a792
|
|||
|
6a7c86a41b
|
|||
|
6277a32fe6
|
|||
|
8f0576d6bc
|
|||
|
f56c40cf00
|
|||
|
28a1237d62
|
|||
|
4c8df56193
|
|||
|
200485246b
|
|||
|
2caebb7d19
|
|||
|
9d9d538fe6
|
|||
|
3bdffd03db
|
|||
|
a710692725
|
|||
|
859736e5be
|
|||
|
d5b2e43364
|
|||
|
33251eaca7
|
|||
|
63311644da
|
|||
|
8668e85623
|
|||
|
902eaf5a01
|
|||
|
df7c7b9f6b
|
|||
|
bb4b895cb5
|
|||
|
0f1c61ae33
|
|||
|
0359ddf99f
|
|||
|
a93fc3f88d
|
|||
|
d70aedffa8
|
|||
|
dba728e0c4
|
|||
|
544722f8e0
|
|||
|
00110a639a
|
|||
|
5af36f4954
|
|||
|
ac72a72afc
|
|||
|
a6560509d9
|
|||
|
44c28f00d6
|
|||
|
cce2b6ba51
|
|||
|
43363ea4bf
|
|||
|
00fa76cb69
|
|||
|
5e10f0ba54
|
|||
|
eb44b6fb91
|
|||
|
a012011631
|
|||
|
05ac3a0651
|
|||
|
4317c7e581
|
|||
|
8da5f807cf
|
|||
|
1b50a834a5
|
|||
|
38c19edc8b
|
|||
|
c646864805
|
|||
|
c2196df141
|
|||
|
44f2f5d4f5
|
|||
|
25c91f5a77
|
|||
|
796b05c9a5
|
|||
|
9286858573
|
|||
|
95bc91e020
|
|||
|
1754bbcf45
|
|||
|
62d82f38c8
|
|||
|
c063d93dc7
|
|||
|
0317b88c87
|
|||
|
1431188e27
|
|||
|
9cdc641b0a
|
|||
|
7b88e89489
|
|||
|
9c2d659d89
|
|||
|
9fb90607ad
|
|||
|
dd09af34b7
|
|||
|
dcbc8a90b4
|
|||
|
7cbd24dd2f
|
|||
|
1a163bdb8b
|
|||
|
cf19f82875
|
|||
|
9c9453172a
|
|||
|
973c024abe
|
|||
|
dba2b9c982
|
|||
|
b6e1dc4893
|
|||
|
f94550ec61
|
|||
|
69d45fea44
|
|||
|
4697a56760
|
|||
|
4e11970a7e
|
|||
|
ae0b5506ab
|
|||
|
701846ab39
|
|||
|
4927c8c692
|
|||
|
e6341e59bb
|
|||
|
33fb047a73
|
|||
|
215141856a
|
|||
|
086dd66aa9
|
|||
|
8c6fe0ad32
|
|||
|
ca51c9413b
|
|||
|
857917aa92
|
|||
|
51dc56c9df
|
|||
|
bb669743b6
|
|||
|
d590d1da46
|
|||
|
d1d9caaa5e
|
|||
|
5bae7c1bd2
|
|||
|
6f83d1dceb
|
|||
|
ab70b30053
|
|||
|
ea75579b33
|
|||
|
8437164dec
|
|||
|
fec64d5595
|
|||
|
fefb4c16ac
|
|||
|
6baf1a7bbd
|
|||
|
78636fdc18
|
|||
|
e18f729488
|
|||
|
9f1041988d
|
|||
|
fa034a1a6a
|
|||
|
dcc5b6c719
|
|||
|
eca319e5e4
|
|||
|
817f1b6000
|
|||
|
31f428f4ec
|
|||
|
4b4e24e71a
|
|||
|
4e84d6a802
|
|||
|
f94171fcf2
|
|||
|
4131a926f2
|
|||
|
4fcc506d84
|
|||
|
5529727137
|
|||
|
e137afa736
|
|||
|
891420edfd
|
|||
|
0bfd3ad4ce
|
|||
|
e03bc36f63
|
|||
|
d3b34cd482
|
|||
|
b067c1948b
|
|||
|
390e21a72d
|
|||
|
876fda4f55
|
|||
|
60b2395940
|
|||
|
e7c75f8f9b
|
|||
|
a9a9d69a92
|
|||
|
26dead7ea4
|
|||
|
94f6938b9a
|
|||
|
62b1e83541
|
|||
|
bd2e929b77
|
|||
|
9a0647fdfd
|
|||
|
47b7600f5e
|
|||
|
8f98c623ee
|
|||
|
4efda5347c
|
|||
|
23429d9631
|
|||
|
ca57c2632a
|
|||
|
c55500f51a
|
|||
|
1fee920902
|
|||
|
c6096d05b5
|
|||
|
07aa11d78d
|
|||
|
de27dce09c
|
|||
|
a1b2225750
|
|||
|
b87a109d61
|
|||
|
81145064de
|
|||
|
60a8ee7a80
|
|||
|
84f8c9436f
|
|||
|
a8f7532abd
|
|||
|
8dfbd0dee2
|
|||
|
930744e165
|
|||
|
4ca8825e02
|
|||
|
024b5117b4
|
|||
|
ac6b606ccc
|
|||
|
8bba456b14
|
|||
|
bb97445a96
|
|||
|
e2adac72cc
|
|||
|
3ddb0cf205
|
|||
|
efc13db66e
|
|||
|
b6315482b7
|
|||
|
51ea785d83
|
|||
|
5c34a6846a
|
|||
|
da507edd05
|
|||
|
580b68789b
|
|||
|
31f9feab7b
|
|||
|
41bd25e711
|
|||
|
d9435c988c
|
|||
|
919a55c90b
|
|||
|
68bb695054
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/meson.build.user
|
||||
/subprojects/*
|
||||
!/subprojects/*.wrap
|
||||
!/subprojects/packagefiles
|
||||
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
||||
[submodule "wuffs-mirror-release-c"]
|
||||
path = wuffs-mirror-release-c
|
||||
path = submodules/wuffs-mirror-release-c
|
||||
url = https://github.com/google/wuffs-mirror-release-c
|
||||
[submodule "liberty"]
|
||||
path = submodules/liberty
|
||||
url = https://git.janouch.name/p/liberty.git
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
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.
|
||||
|
||||
65
README.adoc
65
README.adoc
@@ -1,7 +1,8 @@
|
||||
fiv
|
||||
===
|
||||
|
||||
'fiv' is a slightly unconventional, general-purpose image browser and viewer.
|
||||
'fiv' is a slightly unconventional, general-purpose image browser and viewer
|
||||
for Linux and Windows (macOS still has major issues).
|
||||
|
||||
image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
|
||||
|
||||
@@ -12,8 +13,10 @@ 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.
|
||||
|
||||
Explicit non-goals
|
||||
------------------
|
||||
@@ -21,7 +24,6 @@ Explicit non-goals
|
||||
nothing beyond the most basic of adjustments is desired.
|
||||
- Following the latest GNOME HIG to the letter--header bars are deliberately
|
||||
avoided, for their general user hostility.
|
||||
- Portability to non-UNIXy systems, although patches are welcome.
|
||||
- Memory efficiency is secondary to both performance and development effort.
|
||||
|
||||
Aspirations
|
||||
@@ -31,28 +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 dependencies: Meson, pkg-config +
|
||||
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, spng>=0.7.0 +
|
||||
Optional dependencies: lcms2, LibRaw, librsvg-2.0, xcursor, libheif, libtiff,
|
||||
ExifTool, resvg (unstable API, needs to be requested explicitly)
|
||||
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
|
||||
$ meson devenv fiv
|
||||
|
||||
Considering the vast amount of dynamically-linked dependencies, do not attempt
|
||||
direct installations. To test the program, help it find its custom thumbnailer:
|
||||
The lossless JPEG cropper and reverse image search are intended to be invoked
|
||||
from a file manager context menu.
|
||||
|
||||
$ PATH=$(pwd):$PATH ./fiv
|
||||
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:
|
||||
|
||||
The lossless JPEG cropper is intended to be invoked from a context menu.
|
||||
$ meson compile deb
|
||||
# dpkg -i fiv-*.deb
|
||||
|
||||
Windows
|
||||
~~~~~~~
|
||||
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
|
||||
dependencies listed at the beginning of 'msys2-configure.sh',
|
||||
plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
|
||||
Beware that the build will take up about a gigabyte of disk space.
|
||||
|
||||
$ sh -e msys2-configure.sh builddir
|
||||
$ meson compile package -C builddir
|
||||
|
||||
If everything succeeds, you will find a portable build of the application
|
||||
in the 'builddir/package' subdirectory, and a very basic MSI installation
|
||||
package in 'builddir'.
|
||||
|
||||
Faster colour management
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
To get the Little CMS fast float plugin, you'll have to enter MSYS2 and
|
||||
https://www.msys2.org/wiki/Creating-Packages/#re-building-a-package[rebuild]
|
||||
_mingw-w64-lcms2_ with the following change:
|
||||
|
||||
sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
85
docs/fiv.adoc
Normal file
85
docs/fiv.adoc
Normal file
@@ -0,0 +1,85 @@
|
||||
fiv(1)
|
||||
======
|
||||
:doctype: manpage
|
||||
:manmanual: fiv Manual
|
||||
:mansource: fiv {release-version}
|
||||
|
||||
Name
|
||||
----
|
||||
fiv - Image browser and viewer
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
*fiv* [_OPTION_]... [_PATH_ | _URI_]...
|
||||
|
||||
Description
|
||||
-----------
|
||||
*fiv* is a general-purpose image browser and viewer: pass it a directory path
|
||||
or URI to open it for browsing, or pass an image to open it for viewing.
|
||||
In case that multiple arguments are passed, they'll be opened as a virtual
|
||||
directory containing all of them.
|
||||
|
||||
For more information concerning usage, press *F1* in the application to open
|
||||
the _User Guide_.
|
||||
|
||||
// TODO(p): Try to merge the two, though this one focuses on command line usage.
|
||||
|
||||
Options
|
||||
-------
|
||||
*--browse*::
|
||||
When an image is passed, start in browsing mode, and preselect that
|
||||
image in its containing directory. This is used by *fiv*'s inode/directory
|
||||
handler to implement the "Open Containing Folder" feature of certain
|
||||
applications.
|
||||
|
||||
*--collection*::
|
||||
Always put arguments in a virtual directory, even when only one is passed.
|
||||
Implies *--browse*.
|
||||
|
||||
*--help-all*::
|
||||
Show the full list of options, including those provided by GTK+.
|
||||
|
||||
*--invalidate-cache*::
|
||||
Invalidate the wide thumbnail cache, removing thumbnails for files that can
|
||||
no longer be found.
|
||||
|
||||
*--list-supported-media-types*::
|
||||
Output supported media types and exit. This is used by a script to update
|
||||
the list of MIME types within *fiv*'s desktop file when the list
|
||||
of GdkPixbuf loaders changes.
|
||||
|
||||
*-V*, *--version*::
|
||||
Output version information and exit.
|
||||
|
||||
Internal options
|
||||
~~~~~~~~~~~~~~~~
|
||||
*--extract-thumbnail*::
|
||||
Present any embedded thumbnail of the first argument on the standard output
|
||||
in an application-specific bitmap format. When both *--thumbnail*
|
||||
and *--extract-thumbnail* are passed, this option takes precedence,
|
||||
exiting early if successful. This is used to enhance responsivity
|
||||
of thumbnail procurement.
|
||||
|
||||
*--thumbnail*=_SIZE_::
|
||||
Generate wide thumbnails for the first argument, in all sizes not exceeding
|
||||
_SIZE_, and present the largest of them on the standard output
|
||||
in an application-specific bitmap format. Available sizes follow directory
|
||||
names in the _Thumbnail Managing Standard_.
|
||||
|
||||
*--thumbnail-for-search*=_SIZE_::
|
||||
Transform the first argument to a widely supported image file format,
|
||||
and present it on the standard output. The image will be downscaled as
|
||||
necessary so as to not exceed _SIZE_ (see *--thumbnail*).
|
||||
|
||||
Reporting bugs
|
||||
--------------
|
||||
Use https://git.janouch.name/p/fiv to report bugs, request features,
|
||||
or submit pull requests.
|
||||
|
||||
See also
|
||||
--------
|
||||
_Desktop Entry Specification_,
|
||||
https://specifications.freedesktop.org/desktop-entry-spec/latest/[].
|
||||
|
||||
_Thumbnail Managing Standard_,
|
||||
https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html[].
|
||||
@@ -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-01-25</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> + <kbd>?</kbd> will give you a convenient overview
|
||||
of all shortcuts. In addition to these, remember that you may often use
|
||||
<kbd>Ctrl</kbd> + <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.
|
||||
@@ -85,8 +86,8 @@ invalidate this cache automatically. Should you find out that your
|
||||
fiv --invalidate-cache
|
||||
</pre>
|
||||
|
||||
<p>to trim it down. Alternatively, if you want to get rid of _all_ thumbnails,
|
||||
even for existing images:
|
||||
<p>to trim it down. Alternatively, if you want to get rid of <i>all</i>
|
||||
thumbnails, even for existing images:
|
||||
|
||||
<pre>
|
||||
rm -rf ~/.cache/thumbnails/wide-*
|
||||
@@ -94,6 +95,13 @@ rm -rf ~/.cache/thumbnails/wide-*
|
||||
|
||||
<h2>Configuration</h2>
|
||||
|
||||
<p>To adjust the few configuration options of <i>fiv</i>,
|
||||
press <kbd>Ctrl</kbd> + <kbd>,</kbd> to open <i>Preferences</i>.
|
||||
|
||||
<p>To make your changes take effect, restart <i>fiv</i>.
|
||||
|
||||
<h3>Theming</h3>
|
||||
|
||||
<p>The standard means to adjust the looks of the program is through GTK+ 3 CSS.
|
||||
As an example, to tightly pack browser items, put the following in your
|
||||
<i>~/.config/gtk-3.0/gtk.css</i>:
|
||||
|
||||
BIN
docs/fiv.webp
BIN
docs/fiv.webp
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 194 KiB |
@@ -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; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Type=Application
|
||||
Name=fiv
|
||||
GenericName=Image Browser
|
||||
X-GNOME-FullName=fiv Image Browser
|
||||
Icon=fiv
|
||||
Exec=fiv --browse -- %u
|
||||
NoDisplay=true
|
||||
|
||||
1158
fiv-browser.c
1158
fiv-browser.c
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fiv-io.h"
|
||||
#include "fiv-io-model.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
|
||||
729
fiv-collection.c
Normal file
729
fiv-collection.c
Normal file
@@ -0,0 +1,729 @@
|
||||
//
|
||||
// fiv-collection.c: GVfs extension for grouping arbitrary files together
|
||||
//
|
||||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include "fiv-collection.h"
|
||||
|
||||
static struct {
|
||||
GFile **files;
|
||||
gsize files_len;
|
||||
} g;
|
||||
|
||||
gboolean
|
||||
fiv_collection_uri_matches(const char *uri)
|
||||
{
|
||||
static const char prefix[] = FIV_COLLECTION_SCHEME ":";
|
||||
return !g_ascii_strncasecmp(uri, prefix, sizeof prefix - 1);
|
||||
}
|
||||
|
||||
GFile **
|
||||
fiv_collection_get_contents(gsize *len)
|
||||
{
|
||||
*len = g.files_len;
|
||||
return g.files;
|
||||
}
|
||||
|
||||
void
|
||||
fiv_collection_reload(gchar **uris)
|
||||
{
|
||||
if (g.files) {
|
||||
for (gsize i = 0; i < g.files_len; i++)
|
||||
g_object_unref(g.files[i]);
|
||||
g_free(g.files);
|
||||
}
|
||||
|
||||
g.files_len = g_strv_length(uris);
|
||||
g.files = g_malloc0_n(g.files_len + 1, sizeof *g.files);
|
||||
for (gsize i = 0; i < g.files_len; i++)
|
||||
g.files[i] = g_file_new_for_uri(uris[i]);
|
||||
}
|
||||
|
||||
// --- Declarations ------------------------------------------------------------
|
||||
|
||||
#define FIV_TYPE_COLLECTION_FILE (fiv_collection_file_get_type())
|
||||
G_DECLARE_FINAL_TYPE(
|
||||
FivCollectionFile, fiv_collection_file, FIV, COLLECTION_FILE, GObject)
|
||||
|
||||
struct _FivCollectionFile {
|
||||
GObject parent_instance;
|
||||
|
||||
gint index; ///< Original index into g.files, or -1
|
||||
GFile *target; ///< The wrapped file, or NULL for root
|
||||
gchar *subpath; ///< Any subpath, rooted at the target
|
||||
};
|
||||
|
||||
#define FIV_TYPE_COLLECTION_ENUMERATOR (fiv_collection_enumerator_get_type())
|
||||
G_DECLARE_FINAL_TYPE(FivCollectionEnumerator, fiv_collection_enumerator, FIV,
|
||||
COLLECTION_ENUMERATOR, GFileEnumerator)
|
||||
|
||||
struct _FivCollectionEnumerator {
|
||||
GFileEnumerator parent_instance;
|
||||
|
||||
gchar *attributes; ///< Attributes to look up
|
||||
gsize index; ///< Root: index into g.files
|
||||
GFileEnumerator *subenumerator; ///< Non-root: a wrapped enumerator
|
||||
};
|
||||
|
||||
// --- Enumerator --------------------------------------------------------------
|
||||
|
||||
G_DEFINE_TYPE(
|
||||
FivCollectionEnumerator, fiv_collection_enumerator, G_TYPE_FILE_ENUMERATOR)
|
||||
|
||||
static void
|
||||
fiv_collection_enumerator_finalize(GObject *object)
|
||||
{
|
||||
FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(object);
|
||||
g_free(self->attributes);
|
||||
g_clear_object(&self->subenumerator);
|
||||
}
|
||||
|
||||
static GFileInfo *
|
||||
fiv_collection_enumerator_next_file(GFileEnumerator *enumerator,
|
||||
GCancellable *cancellable, GError **error)
|
||||
{
|
||||
FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator);
|
||||
if (self->subenumerator) {
|
||||
GFileInfo *info = g_file_enumerator_next_file(
|
||||
self->subenumerator, cancellable, error);
|
||||
if (!info)
|
||||
return NULL;
|
||||
|
||||
// TODO(p): Consider discarding certain classes of attributes
|
||||
// from the results (adjusting "attributes" is generally unreliable).
|
||||
GFile *target = g_file_enumerator_get_child(self->subenumerator, info);
|
||||
gchar *target_uri = g_file_get_uri(target);
|
||||
g_object_unref(target);
|
||||
g_file_info_set_attribute_string(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri);
|
||||
g_free(target_uri);
|
||||
return info;
|
||||
}
|
||||
|
||||
if (self->index >= g.files_len)
|
||||
return NULL;
|
||||
|
||||
FivCollectionFile *file = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
|
||||
file->index = self->index;
|
||||
file->target = g_object_ref(g.files[self->index++]);
|
||||
|
||||
GFileInfo *info = g_file_query_info(G_FILE(file), self->attributes,
|
||||
G_FILE_QUERY_INFO_NONE, cancellable, error);
|
||||
g_object_unref(file);
|
||||
return info;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fiv_collection_enumerator_close(
|
||||
GFileEnumerator *enumerator, GCancellable *cancellable, GError **error)
|
||||
{
|
||||
FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator);
|
||||
if (self->subenumerator)
|
||||
return g_file_enumerator_close(self->subenumerator, cancellable, error);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_collection_enumerator_class_init(FivCollectionEnumeratorClass *klass)
|
||||
{
|
||||
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
||||
object_class->finalize = fiv_collection_enumerator_finalize;
|
||||
|
||||
GFileEnumeratorClass *enumerator_class = G_FILE_ENUMERATOR_CLASS(klass);
|
||||
enumerator_class->next_file = fiv_collection_enumerator_next_file;
|
||||
enumerator_class->close_fn = fiv_collection_enumerator_close;
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_collection_enumerator_init(G_GNUC_UNUSED FivCollectionEnumerator *self)
|
||||
{
|
||||
}
|
||||
|
||||
// --- Proxying GFile implementation -------------------------------------------
|
||||
|
||||
static void fiv_collection_file_file_iface_init(GFileIface *iface);
|
||||
|
||||
G_DEFINE_TYPE_WITH_CODE(FivCollectionFile, fiv_collection_file, G_TYPE_OBJECT,
|
||||
G_IMPLEMENT_INTERFACE(G_TYPE_FILE, fiv_collection_file_file_iface_init))
|
||||
|
||||
static void
|
||||
fiv_collection_file_finalize(GObject *object)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(object);
|
||||
if (self->target)
|
||||
g_object_unref(self->target);
|
||||
g_free(self->subpath);
|
||||
}
|
||||
|
||||
static GFile *
|
||||
fiv_collection_file_dup(GFile *file)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
|
||||
if (self->target)
|
||||
new->target = g_object_ref(self->target);
|
||||
new->subpath = g_strdup(self->subpath);
|
||||
return G_FILE(new);
|
||||
}
|
||||
|
||||
static guint
|
||||
fiv_collection_file_hash(GFile *file)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
guint hash = g_int_hash(&self->index);
|
||||
if (self->target)
|
||||
hash ^= g_file_hash(self->target);
|
||||
if (self->subpath)
|
||||
hash ^= g_str_hash(self->subpath);
|
||||
return hash;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fiv_collection_file_equal(GFile *file1, GFile *file2)
|
||||
{
|
||||
FivCollectionFile *cf1 = FIV_COLLECTION_FILE(file1);
|
||||
FivCollectionFile *cf2 = FIV_COLLECTION_FILE(file2);
|
||||
return cf1->index == cf2->index && cf1->target == cf2->target &&
|
||||
!g_strcmp0(cf1->subpath, cf2->subpath);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fiv_collection_file_is_native(G_GNUC_UNUSED GFile *file)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fiv_collection_file_has_uri_scheme(
|
||||
G_GNUC_UNUSED GFile *file, const char *uri_scheme)
|
||||
{
|
||||
return !g_ascii_strcasecmp(uri_scheme, FIV_COLLECTION_SCHEME);
|
||||
}
|
||||
|
||||
static char *
|
||||
fiv_collection_file_get_uri_scheme(G_GNUC_UNUSED GFile *file)
|
||||
{
|
||||
return g_strdup(FIV_COLLECTION_SCHEME);
|
||||
}
|
||||
|
||||
static char *
|
||||
get_prefixed_name(FivCollectionFile *self, const char *name)
|
||||
{
|
||||
return g_strdup_printf("%d. %s", self->index + 1, name);
|
||||
}
|
||||
|
||||
static char *
|
||||
get_target_basename(FivCollectionFile *self)
|
||||
{
|
||||
g_return_val_if_fail(self->target != NULL, g_strdup(""));
|
||||
|
||||
// The "http" scheme doesn't behave nicely, make something up if needed.
|
||||
// Foreign roots likewise need to be fixed up for our needs.
|
||||
gchar *basename = g_file_get_basename(self->target);
|
||||
if (!basename || *basename == '/') {
|
||||
g_free(basename);
|
||||
basename = g_file_get_uri_scheme(self->target);
|
||||
}
|
||||
|
||||
gchar *name = get_prefixed_name(self, basename);
|
||||
g_free(basename);
|
||||
return name;
|
||||
}
|
||||
|
||||
static char *
|
||||
fiv_collection_file_get_basename(GFile *file)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
if (!self->target)
|
||||
return g_strdup("/");
|
||||
if (self->subpath)
|
||||
return g_path_get_basename(self->subpath);
|
||||
return get_target_basename(self);
|
||||
}
|
||||
|
||||
static char *
|
||||
fiv_collection_file_get_path(G_GNUC_UNUSED GFile *file)
|
||||
{
|
||||
// This doesn't seem to be worth implementing (for compatible targets).
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *
|
||||
get_unescaped_uri(FivCollectionFile *self)
|
||||
{
|
||||
GString *unescaped = g_string_new(FIV_COLLECTION_SCHEME ":/");
|
||||
if (!self->target)
|
||||
return g_string_free(unescaped, FALSE);
|
||||
|
||||
gchar *basename = get_target_basename(self);
|
||||
g_string_append(unescaped, basename);
|
||||
g_free(basename);
|
||||
if (self->subpath)
|
||||
g_string_append(g_string_append(unescaped, "/"), self->subpath);
|
||||
return g_string_free(unescaped, FALSE);
|
||||
}
|
||||
|
||||
static char *
|
||||
fiv_collection_file_get_uri(GFile *file)
|
||||
{
|
||||
gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file));
|
||||
gchar *uri = g_uri_escape_string(
|
||||
unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
|
||||
g_free(unescaped);
|
||||
return uri;
|
||||
}
|
||||
|
||||
static char *
|
||||
fiv_collection_file_get_parse_name(GFile *file)
|
||||
{
|
||||
gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file));
|
||||
gchar *parse_name = g_uri_escape_string(
|
||||
unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH " ", TRUE);
|
||||
g_free(unescaped);
|
||||
return parse_name;
|
||||
}
|
||||
|
||||
static GFile *
|
||||
fiv_collection_file_get_parent(GFile *file)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
if (!self->target)
|
||||
return NULL;
|
||||
|
||||
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
|
||||
if (self->subpath) {
|
||||
new->index = self->index;
|
||||
new->target = g_object_ref(self->target);
|
||||
if (strchr(self->subpath, '/'))
|
||||
new->subpath = g_path_get_dirname(self->subpath);
|
||||
}
|
||||
return G_FILE(new);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fiv_collection_file_prefix_matches(GFile *prefix, GFile *file)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
FivCollectionFile *parent = FIV_COLLECTION_FILE(prefix);
|
||||
|
||||
// The root has no parents.
|
||||
if (!self->target)
|
||||
return FALSE;
|
||||
|
||||
// The root prefixes everything that is not the root.
|
||||
if (!parent->target)
|
||||
return TRUE;
|
||||
|
||||
if (self->index != parent->index || !self->subpath)
|
||||
return FALSE;
|
||||
if (!parent->subpath)
|
||||
return TRUE;
|
||||
|
||||
return g_str_has_prefix(self->subpath, parent->subpath) &&
|
||||
self->subpath[strlen(parent->subpath)] == '/';
|
||||
}
|
||||
|
||||
// This virtual method seems to be intended for local files only,
|
||||
// and documentation claims that the result is in filesystem encoding.
|
||||
// For us, paths are mostly opaque strings of arbitrary encoding, however.
|
||||
static char *
|
||||
fiv_collection_file_get_relative_path(GFile *parent, GFile *descendant)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(descendant);
|
||||
FivCollectionFile *prefix = FIV_COLLECTION_FILE(parent);
|
||||
if (!fiv_collection_file_prefix_matches(parent, descendant))
|
||||
return NULL;
|
||||
|
||||
g_assert((!prefix->target && self->target) ||
|
||||
(prefix->target && self->target && self->subpath));
|
||||
|
||||
if (!prefix->target) {
|
||||
gchar *basename = get_target_basename(self);
|
||||
gchar *path = g_build_path("/", basename, self->subpath, NULL);
|
||||
g_free(basename);
|
||||
return path;
|
||||
}
|
||||
|
||||
return prefix->subpath
|
||||
? g_strdup(self->subpath + strlen(prefix->subpath) + 1)
|
||||
: g_strdup(self->subpath);
|
||||
}
|
||||
|
||||
static GFile *
|
||||
get_file_for_path(const char *path)
|
||||
{
|
||||
// Skip all initial slashes, making the result relative to the root.
|
||||
if (!*(path += strspn(path, "/")))
|
||||
return g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
|
||||
|
||||
char *end = NULL;
|
||||
guint64 i = g_ascii_strtoull(path, &end, 10);
|
||||
if (i <= 0 || i > g.files_len || *end != '.')
|
||||
return g_file_new_for_uri("");
|
||||
|
||||
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
|
||||
new->index = --i;
|
||||
new->target = g_object_ref(g.files[i]);
|
||||
|
||||
const char *subpath = strchr(path, '/');
|
||||
if (subpath && subpath[1])
|
||||
new->subpath = g_strdup(++subpath);
|
||||
return G_FILE(new);
|
||||
}
|
||||
|
||||
static GFile *
|
||||
fiv_collection_file_resolve_relative_path(
|
||||
GFile *file, const char *relative_path)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
if (!self->target)
|
||||
return get_file_for_path(relative_path);
|
||||
|
||||
gchar *basename = get_target_basename(self);
|
||||
gchar *root = g_build_path("/", "/", basename, self->subpath, NULL);
|
||||
g_free(basename);
|
||||
gchar *canonicalized = g_canonicalize_filename(relative_path, root);
|
||||
GFile *result = get_file_for_path(canonicalized);
|
||||
g_free(canonicalized);
|
||||
return result;
|
||||
}
|
||||
|
||||
static GFile *
|
||||
get_target_subpathed(FivCollectionFile *self)
|
||||
{
|
||||
return self->subpath
|
||||
? g_file_resolve_relative_path(self->target, self->subpath)
|
||||
: g_object_ref(self->target);
|
||||
}
|
||||
|
||||
static GFile *
|
||||
fiv_collection_file_get_child_for_display_name(
|
||||
GFile *file, const char *display_name, GError **error)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
if (!self->target)
|
||||
return get_file_for_path(display_name);
|
||||
|
||||
// Implementations often redirect to g_file_resolve_relative_path().
|
||||
// We don't want to go up (and possibly receive a "/" basename),
|
||||
// nor do we want to skip path elements.
|
||||
// TODO(p): This should still be implementable, via URI inspection.
|
||||
if (strchr(display_name, '/')) {
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Display name must not contain path separators");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GFile *intermediate = get_target_subpathed(self);
|
||||
GFile *resolved =
|
||||
g_file_get_child_for_display_name(intermediate, display_name, error);
|
||||
g_object_unref(intermediate);
|
||||
if (!resolved)
|
||||
return NULL;
|
||||
|
||||
// Try to retrieve the display name converted to whatever insanity
|
||||
// the target might have chosen to encode its paths with.
|
||||
gchar *converted = g_file_get_basename(resolved);
|
||||
g_object_unref(resolved);
|
||||
|
||||
FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
|
||||
new->index = self->index;
|
||||
new->target = g_object_ref(self->target);
|
||||
new->subpath = self->subpath
|
||||
? g_build_path("/", self->subpath, converted, NULL)
|
||||
: g_strdup(converted);
|
||||
g_free(converted);
|
||||
return G_FILE(new);
|
||||
}
|
||||
|
||||
static GFileEnumerator *
|
||||
fiv_collection_file_enumerate_children(GFile *file, const char *attributes,
|
||||
GFileQueryInfoFlags flags, GCancellable *cancellable, GError **error)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
FivCollectionEnumerator *enumerator = g_object_new(
|
||||
FIV_TYPE_COLLECTION_ENUMERATOR, "container", file, NULL);
|
||||
enumerator->attributes = g_strdup(attributes);
|
||||
if (self->target) {
|
||||
GFile *intermediate = get_target_subpathed(self);
|
||||
enumerator->subenumerator = g_file_enumerate_children(
|
||||
intermediate, enumerator->attributes, flags, cancellable, error);
|
||||
g_object_unref(intermediate);
|
||||
}
|
||||
return G_FILE_ENUMERATOR(enumerator);
|
||||
}
|
||||
|
||||
// TODO(p): Implement async variants of this proxying method.
|
||||
static GFileInfo *
|
||||
fiv_collection_file_query_info(GFile *file, const char *attributes,
|
||||
GFileQueryInfoFlags flags, GCancellable *cancellable,
|
||||
G_GNUC_UNUSED GError **error)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
GError *e = NULL;
|
||||
if (!self->target) {
|
||||
GFileInfo *info = g_file_info_new();
|
||||
g_file_info_set_file_type(info, G_FILE_TYPE_DIRECTORY);
|
||||
g_file_info_set_name(info, "/");
|
||||
g_file_info_set_display_name(info, "Collection");
|
||||
|
||||
GIcon *icon = g_icon_new_for_string("shapes-symbolic", NULL);
|
||||
if (icon) {
|
||||
g_file_info_set_symbolic_icon(info, icon);
|
||||
g_object_unref(icon);
|
||||
} else {
|
||||
g_warning("%s", e->message);
|
||||
g_error_free(e);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// The "http" scheme doesn't behave nicely, make something up if needed.
|
||||
GFile *intermediate = get_target_subpathed(self);
|
||||
GFileInfo *info =
|
||||
g_file_query_info(intermediate, attributes, flags, cancellable, &e);
|
||||
if (!info) {
|
||||
g_warning("%s", e->message);
|
||||
g_error_free(e);
|
||||
|
||||
info = g_file_info_new();
|
||||
g_file_info_set_file_type(info, G_FILE_TYPE_REGULAR);
|
||||
gchar *basename = g_file_get_basename(intermediate);
|
||||
g_file_info_set_name(info, basename);
|
||||
|
||||
// The display name is "guaranteed to always be set" when queried,
|
||||
// which is up to implementations.
|
||||
gchar *safe = g_utf8_make_valid(basename, -1);
|
||||
g_free(basename);
|
||||
g_file_info_set_display_name(info, safe);
|
||||
g_free(safe);
|
||||
}
|
||||
|
||||
gchar *target_uri = g_file_get_uri(intermediate);
|
||||
g_file_info_set_attribute_string(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri);
|
||||
g_free(target_uri);
|
||||
g_object_unref(intermediate);
|
||||
|
||||
// Ensure all basenames that might have been set have the numeric prefix.
|
||||
const char *name = NULL;
|
||||
if (!self->subpath) {
|
||||
// Always set this, because various schemes may not do so themselves,
|
||||
// which then troubles GFileEnumerator.
|
||||
gchar *basename = get_target_basename(self);
|
||||
g_file_info_set_name(info, basename);
|
||||
g_free(basename);
|
||||
|
||||
if (g_file_info_has_attribute(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) &&
|
||||
(name = g_file_info_get_display_name(info))) {
|
||||
gchar *prefixed = get_prefixed_name(self, name);
|
||||
g_file_info_set_display_name(info, prefixed);
|
||||
g_free(prefixed);
|
||||
}
|
||||
if (g_file_info_has_attribute(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME) &&
|
||||
(name = g_file_info_get_edit_name(info))) {
|
||||
gchar *prefixed = get_prefixed_name(self, name);
|
||||
g_file_info_set_edit_name(info, prefixed);
|
||||
g_free(prefixed);
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
static GFileInfo *
|
||||
fiv_collection_file_query_filesystem_info(G_GNUC_UNUSED GFile *file,
|
||||
G_GNUC_UNUSED const char *attributes,
|
||||
G_GNUC_UNUSED GCancellable *cancellable, G_GNUC_UNUSED GError **error)
|
||||
{
|
||||
GFileInfo *info = g_file_info_new();
|
||||
GFileAttributeMatcher *matcher = g_file_attribute_matcher_new(attributes);
|
||||
if (g_file_attribute_matcher_matches(
|
||||
matcher, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE)) {
|
||||
g_file_info_set_attribute_string(
|
||||
info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, FIV_COLLECTION_SCHEME);
|
||||
}
|
||||
if (g_file_attribute_matcher_matches(
|
||||
matcher, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) {
|
||||
g_file_info_set_attribute_boolean(
|
||||
info, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, TRUE);
|
||||
}
|
||||
|
||||
g_file_attribute_matcher_unref(matcher);
|
||||
return info;
|
||||
}
|
||||
|
||||
static GFile *
|
||||
fiv_collection_file_set_display_name(G_GNUC_UNUSED GFile *file,
|
||||
G_GNUC_UNUSED const char *display_name,
|
||||
G_GNUC_UNUSED GCancellable *cancellable, GError **error)
|
||||
{
|
||||
g_set_error_literal(
|
||||
error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Operation not supported");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static GFileInputStream *
|
||||
fiv_collection_file_read(GFile *file, GCancellable *cancellable, GError **error)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
if (!self->target) {
|
||||
g_set_error_literal(
|
||||
error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GFile *intermediate = get_target_subpathed(self);
|
||||
GFileInputStream *stream = g_file_read(intermediate, cancellable, error);
|
||||
g_object_unref(intermediate);
|
||||
return stream;
|
||||
}
|
||||
|
||||
static void
|
||||
on_read(GObject *source_object, GAsyncResult *res, gpointer user_data)
|
||||
{
|
||||
GFile *intermediate = G_FILE(source_object);
|
||||
GTask *task = G_TASK(user_data);
|
||||
GError *error = NULL;
|
||||
GFileInputStream *result = g_file_read_finish(intermediate, res, &error);
|
||||
if (result)
|
||||
g_task_return_pointer(task, result, g_object_unref);
|
||||
else
|
||||
g_task_return_error(task, error);
|
||||
g_object_unref(task);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_collection_file_read_async(GFile *file, int io_priority,
|
||||
GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
|
||||
{
|
||||
FivCollectionFile *self = FIV_COLLECTION_FILE(file);
|
||||
GTask *task = g_task_new(file, cancellable, callback, user_data);
|
||||
g_task_set_name(task, __func__);
|
||||
g_task_set_priority(task, io_priority);
|
||||
if (!self->target) {
|
||||
g_task_return_new_error(
|
||||
task, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory");
|
||||
g_object_unref(task);
|
||||
return;
|
||||
}
|
||||
|
||||
GFile *intermediate = get_target_subpathed(self);
|
||||
g_file_read_async(intermediate, io_priority, cancellable, on_read, task);
|
||||
g_object_unref(intermediate);
|
||||
}
|
||||
|
||||
static GFileInputStream *
|
||||
fiv_collection_file_read_finish(
|
||||
G_GNUC_UNUSED GFile *file, GAsyncResult *res, GError **error)
|
||||
{
|
||||
return g_task_propagate_pointer(G_TASK(res), error);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_collection_file_file_iface_init(GFileIface *iface)
|
||||
{
|
||||
// Required methods that would segfault if unimplemented.
|
||||
iface->dup = fiv_collection_file_dup;
|
||||
iface->hash = fiv_collection_file_hash;
|
||||
iface->equal = fiv_collection_file_equal;
|
||||
iface->is_native = fiv_collection_file_is_native;
|
||||
iface->has_uri_scheme = fiv_collection_file_has_uri_scheme;
|
||||
iface->get_uri_scheme = fiv_collection_file_get_uri_scheme;
|
||||
iface->get_basename = fiv_collection_file_get_basename;
|
||||
iface->get_path = fiv_collection_file_get_path;
|
||||
iface->get_uri = fiv_collection_file_get_uri;
|
||||
iface->get_parse_name = fiv_collection_file_get_parse_name;
|
||||
iface->get_parent = fiv_collection_file_get_parent;
|
||||
iface->prefix_matches = fiv_collection_file_prefix_matches;
|
||||
iface->get_relative_path = fiv_collection_file_get_relative_path;
|
||||
iface->resolve_relative_path = fiv_collection_file_resolve_relative_path;
|
||||
iface->get_child_for_display_name =
|
||||
fiv_collection_file_get_child_for_display_name;
|
||||
iface->set_display_name = fiv_collection_file_set_display_name;
|
||||
|
||||
// Optional methods.
|
||||
iface->enumerate_children = fiv_collection_file_enumerate_children;
|
||||
iface->query_info = fiv_collection_file_query_info;
|
||||
iface->query_filesystem_info = fiv_collection_file_query_filesystem_info;
|
||||
iface->read_fn = fiv_collection_file_read;
|
||||
iface->read_async = fiv_collection_file_read_async;
|
||||
iface->read_finish = fiv_collection_file_read_finish;
|
||||
|
||||
iface->supports_thread_contexts = TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_collection_file_class_init(FivCollectionFileClass *klass)
|
||||
{
|
||||
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
||||
object_class->finalize = fiv_collection_file_finalize;
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_collection_file_init(FivCollectionFile *self)
|
||||
{
|
||||
self->index = -1;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static GFile *
|
||||
get_file_for_uri(G_GNUC_UNUSED GVfs *vfs, const char *identifier,
|
||||
G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
static const char prefix[] = FIV_COLLECTION_SCHEME ":";
|
||||
const char *path = identifier + sizeof prefix - 1;
|
||||
if (!g_str_has_prefix(identifier, prefix))
|
||||
return NULL;
|
||||
|
||||
// Specifying the authority is not supported.
|
||||
if (g_str_has_prefix(path, "//"))
|
||||
return NULL;
|
||||
|
||||
// Otherwise, it needs to look like an absolute path.
|
||||
if (!g_str_has_prefix(path, "/"))
|
||||
return NULL;
|
||||
|
||||
// TODO(p): Figure out what to do about queries and fragments.
|
||||
// GDummyFile carries them across level, which seems rather arbitrary.
|
||||
const char *trailing = strpbrk(path, "?#");
|
||||
gchar *unescaped = g_uri_unescape_segment(path, trailing, "/");
|
||||
if (!unescaped)
|
||||
return NULL;
|
||||
|
||||
GFile *result = get_file_for_path(unescaped);
|
||||
g_free(unescaped);
|
||||
return result;
|
||||
}
|
||||
|
||||
static GFile *
|
||||
parse_name(GVfs *vfs, const char *identifier, gpointer user_data)
|
||||
{
|
||||
// get_file_for_uri() already parses a superset of URIs.
|
||||
return get_file_for_uri(vfs, identifier, user_data);
|
||||
}
|
||||
|
||||
void
|
||||
fiv_collection_register(void)
|
||||
{
|
||||
GVfs *vfs = g_vfs_get_default();
|
||||
if (!g_vfs_register_uri_scheme(vfs, FIV_COLLECTION_SCHEME,
|
||||
get_file_for_uri, NULL, NULL, parse_name, NULL, NULL))
|
||||
g_warning(FIV_COLLECTION_SCHEME " scheme registration failed");
|
||||
}
|
||||
25
fiv-collection.h
Normal file
25
fiv-collection.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// fiv-collection.h: GVfs extension for grouping arbitrary files together
|
||||
//
|
||||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
#define FIV_COLLECTION_SCHEME "collection"
|
||||
|
||||
gboolean fiv_collection_uri_matches(const char *uri);
|
||||
GFile **fiv_collection_get_contents(gsize *len);
|
||||
void fiv_collection_reload(gchar **uris);
|
||||
void fiv_collection_register(void);
|
||||
573
fiv-context-menu.c
Normal file
573
fiv-context-menu.c
Normal file
@@ -0,0 +1,573 @@
|
||||
//
|
||||
// fiv-context-menu.c: popup menu
|
||||
//
|
||||
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "fiv-collection.h"
|
||||
#include "fiv-context-menu.h"
|
||||
|
||||
G_DEFINE_QUARK(fiv-context-menu-cancellable-quark, fiv_context_menu_cancellable)
|
||||
|
||||
static GtkWidget *
|
||||
info_start_group(GtkWidget *vbox, const char *group)
|
||||
{
|
||||
GtkWidget *label = gtk_label_new(group);
|
||||
gtk_widget_set_hexpand(label, TRUE);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_FILL);
|
||||
PangoAttrList *attrs = pango_attr_list_new();
|
||||
pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
|
||||
gtk_label_set_attributes(GTK_LABEL(label), attrs);
|
||||
pango_attr_list_unref(attrs);
|
||||
|
||||
GtkWidget *grid = gtk_grid_new();
|
||||
GtkWidget *expander = gtk_expander_new(NULL);
|
||||
gtk_expander_set_label_widget(GTK_EXPANDER(expander), label);
|
||||
gtk_expander_set_expanded(GTK_EXPANDER(expander), TRUE);
|
||||
gtk_container_add(GTK_CONTAINER(expander), grid);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
|
||||
gtk_box_pack_start(GTK_BOX(vbox), expander, FALSE, FALSE, 0);
|
||||
return grid;
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
info_parse(char *tsv)
|
||||
{
|
||||
GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
|
||||
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
|
||||
const char *last_group = NULL;
|
||||
GtkWidget *grid = NULL;
|
||||
int line = 1, row = 0;
|
||||
for (char *nl; (nl = strchr(tsv, '\n')); line++, tsv = ++nl) {
|
||||
*nl = 0;
|
||||
if (nl > tsv && nl[-1] == '\r')
|
||||
nl[-1] = 0;
|
||||
|
||||
char *group = tsv, *tag = strchr(group, '\t');
|
||||
if (!tag) {
|
||||
g_warning("ExifTool parse error on line %d", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
*tag++ = 0;
|
||||
for (char *p = group; *p; p++)
|
||||
if (*p == '_')
|
||||
*p = ' ';
|
||||
|
||||
char *value = strchr(tag, '\t');
|
||||
if (!value) {
|
||||
g_warning("ExifTool parse error on line %d", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
*value++ = 0;
|
||||
if (!last_group || strcmp(last_group, group)) {
|
||||
grid = info_start_group(vbox, (last_group = group));
|
||||
row = 0;
|
||||
}
|
||||
|
||||
GtkWidget *a = gtk_label_new(tag);
|
||||
gtk_size_group_add_widget(sg, a);
|
||||
gtk_label_set_selectable(GTK_LABEL(a), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(a), 0.);
|
||||
gtk_grid_attach(GTK_GRID(grid), a, 0, row, 1, 1);
|
||||
|
||||
GtkWidget *b = gtk_label_new(value);
|
||||
gtk_label_set_selectable(GTK_LABEL(b), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(b), 0.);
|
||||
gtk_label_set_line_wrap(GTK_LABEL(b), TRUE);
|
||||
gtk_widget_set_hexpand(b, TRUE);
|
||||
gtk_grid_attach(GTK_GRID(grid), b, 1, row, 1, 1);
|
||||
row++;
|
||||
}
|
||||
g_object_unref(sg);
|
||||
return vbox;
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
info_make_bar(const char *message)
|
||||
{
|
||||
GtkWidget *info = gtk_info_bar_new();
|
||||
gtk_info_bar_set_message_type(GTK_INFO_BAR(info), GTK_MESSAGE_WARNING);
|
||||
GtkWidget *info_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info));
|
||||
// When the label is made selectable, Escape doesn't work when it has focus.
|
||||
gtk_container_add(GTK_CONTAINER(info_area), gtk_label_new(message));
|
||||
return info;
|
||||
}
|
||||
|
||||
static void
|
||||
info_redirect_error(gpointer dialog, GError *error)
|
||||
{
|
||||
// The dialog has been closed and destroyed.
|
||||
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
|
||||
GtkContainer *content_area =
|
||||
GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog)));
|
||||
gtk_container_foreach(content_area, (GtkCallback) gtk_widget_destroy, NULL);
|
||||
gtk_container_add(content_area, info_make_bar(error->message));
|
||||
if (g_error_matches(error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) {
|
||||
gtk_box_pack_start(GTK_BOX(content_area),
|
||||
gtk_label_new("Please install ExifTool."), TRUE, FALSE, 12);
|
||||
}
|
||||
|
||||
g_error_free(error);
|
||||
gtk_widget_show_all(GTK_WIDGET(dialog));
|
||||
}
|
||||
|
||||
static gchar *
|
||||
bytes_to_utf8(GBytes *bytes)
|
||||
{
|
||||
gsize length = 0;
|
||||
gconstpointer data = g_bytes_get_data(bytes, &length);
|
||||
gchar *utf8 = data ? g_utf8_make_valid(data, length) : g_strdup("");
|
||||
g_bytes_unref(bytes);
|
||||
return utf8;
|
||||
}
|
||||
|
||||
static void
|
||||
on_info_finished(GObject *source_object, GAsyncResult *res, gpointer user_data)
|
||||
{
|
||||
GError *error = NULL;
|
||||
GBytes *bytes_out = NULL, *bytes_err = NULL;
|
||||
if (!g_subprocess_communicate_finish(
|
||||
G_SUBPROCESS(source_object), res, &bytes_out, &bytes_err, &error)) {
|
||||
info_redirect_error(user_data, error);
|
||||
return;
|
||||
}
|
||||
|
||||
gchar *out = bytes_to_utf8(bytes_out);
|
||||
gchar *err = bytes_to_utf8(bytes_err);
|
||||
|
||||
GtkWidget *dialog = GTK_WIDGET(user_data);
|
||||
GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||
gtk_container_foreach(
|
||||
GTK_CONTAINER(content_area), (GtkCallback) gtk_widget_destroy, NULL);
|
||||
|
||||
GtkWidget *scroller = gtk_scrolled_window_new(NULL, NULL);
|
||||
gtk_box_pack_start(GTK_BOX(content_area), scroller, TRUE, TRUE, 0);
|
||||
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_container_add(GTK_CONTAINER(scroller), vbox);
|
||||
if (*err)
|
||||
gtk_container_add(GTK_CONTAINER(vbox), info_make_bar(g_strstrip(err)));
|
||||
|
||||
GtkWidget *info = info_parse(out);
|
||||
gtk_style_context_add_class(
|
||||
gtk_widget_get_style_context(info), "fiv-information");
|
||||
gtk_box_pack_start(GTK_BOX(vbox), info, TRUE, TRUE, 0);
|
||||
|
||||
g_free(out);
|
||||
g_free(err);
|
||||
gtk_widget_show_all(dialog);
|
||||
gtk_widget_grab_focus(scroller);
|
||||
}
|
||||
|
||||
static void
|
||||
info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
|
||||
{
|
||||
int flags = G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE;
|
||||
if (bytes_in)
|
||||
flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
|
||||
|
||||
GSubprocessLauncher *launcher = g_subprocess_launcher_new(flags);
|
||||
#ifdef G_OS_WIN32
|
||||
// Both to find wperl, and then to let wperl find the nearby exiftool.
|
||||
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
|
||||
g_subprocess_launcher_set_cwd(launcher, prefix);
|
||||
g_free(prefix);
|
||||
#endif
|
||||
|
||||
// TODO(p): Add a fallback to internal capabilities.
|
||||
// The simplest is to specify the filename and the resolution.
|
||||
GError *error = NULL;
|
||||
GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
|
||||
#ifdef G_OS_WIN32
|
||||
"wperl",
|
||||
#endif
|
||||
"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
|
||||
"--binary", "-quiet", "--", path, NULL);
|
||||
g_object_unref(launcher);
|
||||
if (error) {
|
||||
info_redirect_error(dialog, error);
|
||||
return;
|
||||
}
|
||||
|
||||
GCancellable *cancellable = g_object_get_qdata(
|
||||
G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
|
||||
g_subprocess_communicate_async(
|
||||
subprocess, bytes_in, cancellable, on_info_finished, dialog);
|
||||
g_object_unref(subprocess);
|
||||
}
|
||||
|
||||
static void
|
||||
on_info_loaded(GObject *source_object, GAsyncResult *res, gpointer user_data)
|
||||
{
|
||||
gchar *file_data = NULL;
|
||||
gsize file_len = 0;
|
||||
GError *error = NULL;
|
||||
if (!g_file_load_contents_finish(
|
||||
G_FILE(source_object), res, &file_data, &file_len, NULL, &error)) {
|
||||
info_redirect_error(user_data, error);
|
||||
return;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = GTK_WIDGET(user_data);
|
||||
GBytes *bytes_in = g_bytes_new_take(file_data, file_len);
|
||||
info_spawn(dialog, "-", bytes_in);
|
||||
g_bytes_unref(bytes_in);
|
||||
}
|
||||
|
||||
static void
|
||||
on_info_queried(GObject *source_object, GAsyncResult *res, gpointer user_data)
|
||||
{
|
||||
GFile *file = G_FILE(source_object);
|
||||
GError *error = NULL;
|
||||
GFileInfo *info = g_file_query_info_finish(file, res, &error);
|
||||
gboolean cancelled =
|
||||
error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
|
||||
g_clear_error(&error);
|
||||
if (cancelled)
|
||||
return;
|
||||
|
||||
gchar *path = NULL;
|
||||
const char *target_uri = g_file_info_get_attribute_string(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
|
||||
if (target_uri) {
|
||||
GFile *target = g_file_new_for_uri(target_uri);
|
||||
path = g_file_get_path(target);
|
||||
g_object_unref(target);
|
||||
}
|
||||
g_object_unref(info);
|
||||
|
||||
GtkWidget *dialog = GTK_WIDGET(user_data);
|
||||
GCancellable *cancellable = g_object_get_qdata(
|
||||
G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
|
||||
if (path) {
|
||||
info_spawn(dialog, path, NULL);
|
||||
g_free(path);
|
||||
} else {
|
||||
g_file_load_contents_async(file, cancellable, on_info_loaded, dialog);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
fiv_context_menu_information(GtkWindow *parent, const char *uri)
|
||||
{
|
||||
GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
|
||||
"use-header-bar", TRUE,
|
||||
"title", "Information",
|
||||
"transient-for", parent,
|
||||
"destroy-with-parent", TRUE, NULL);
|
||||
|
||||
// When the window closes, we cancel all asynchronous calls.
|
||||
GCancellable *cancellable = g_cancellable_new();
|
||||
g_object_set_qdata_full(G_OBJECT(dialog),
|
||||
fiv_context_menu_cancellable_quark(), cancellable, g_object_unref);
|
||||
g_signal_connect_swapped(
|
||||
dialog, "destroy", G_CALLBACK(g_cancellable_cancel), cancellable);
|
||||
|
||||
GtkWidget *spinner = gtk_spinner_new();
|
||||
gtk_spinner_start(GTK_SPINNER(spinner));
|
||||
gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
|
||||
spinner, TRUE, TRUE, 12);
|
||||
gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 800);
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
// Mostly to identify URIs with no local path--we pipe these into ExifTool.
|
||||
GFile *file = g_file_new_for_uri(uri);
|
||||
gchar *parse_name = g_file_get_parse_name(file);
|
||||
gtk_header_bar_set_subtitle(
|
||||
GTK_HEADER_BAR(gtk_dialog_get_header_bar(GTK_DIALOG(dialog))),
|
||||
parse_name);
|
||||
g_free(parse_name);
|
||||
|
||||
gchar *path = g_file_get_path(file);
|
||||
if (path) {
|
||||
info_spawn(dialog, path, NULL);
|
||||
g_free(path);
|
||||
} else {
|
||||
// Several GVfs schemes contain pseudo-symlinks
|
||||
// that don't give out filesystem paths directly.
|
||||
g_file_query_info_async(file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
|
||||
G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable,
|
||||
on_info_queried, dialog);
|
||||
}
|
||||
g_object_unref(file);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
typedef struct _OpenContext {
|
||||
GWeakRef window; ///< Parent window for any dialogs
|
||||
GFile *file; ///< The file in question
|
||||
gchar *content_type;
|
||||
GAppInfo *app_info;
|
||||
} OpenContext;
|
||||
|
||||
static void
|
||||
open_context_finalize(gpointer data)
|
||||
{
|
||||
OpenContext *self = data;
|
||||
g_weak_ref_clear(&self->window);
|
||||
g_clear_object(&self->app_info);
|
||||
g_clear_object(&self->file);
|
||||
g_free(self->content_type);
|
||||
}
|
||||
|
||||
static void
|
||||
open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
|
||||
{
|
||||
g_rc_box_release_full(data, open_context_finalize);
|
||||
}
|
||||
|
||||
static void
|
||||
show_error_dialog(GtkWindow *parent, GError *error)
|
||||
{
|
||||
GtkWidget *dialog =
|
||||
gtk_message_dialog_new(GTK_WINDOW(parent), GTK_DIALOG_MODAL,
|
||||
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
|
||||
gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
gtk_widget_destroy(dialog);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
static void
|
||||
open_context_launch(GtkWidget *widget, OpenContext *self)
|
||||
{
|
||||
GdkAppLaunchContext *context =
|
||||
gdk_display_get_app_launch_context(gtk_widget_get_display(widget));
|
||||
gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget));
|
||||
gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time());
|
||||
|
||||
GList *files = g_list_append(NULL, self->file);
|
||||
GError *error = NULL;
|
||||
if (g_app_info_launch(
|
||||
self->app_info, files, G_APP_LAUNCH_CONTEXT(context), &error)) {
|
||||
(void) g_app_info_set_as_last_used_for_type(
|
||||
self->app_info, self->content_type, NULL);
|
||||
} else {
|
||||
GtkWindow *window = g_weak_ref_get(&self->window);
|
||||
show_error_dialog(window, error);
|
||||
g_clear_object(&window);
|
||||
}
|
||||
g_list_free(files);
|
||||
g_object_unref(context);
|
||||
}
|
||||
|
||||
static void
|
||||
append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)
|
||||
{
|
||||
OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
|
||||
g_weak_ref_init(&ctx->window, NULL);
|
||||
ctx->file = g_object_ref(template->file);
|
||||
ctx->content_type = g_strdup(template->content_type);
|
||||
ctx->app_info = opener;
|
||||
|
||||
// On Linux, this prefers the obsoleted X-GNOME-FullName.
|
||||
gchar *name =
|
||||
g_strdup_printf("Open With %s", g_app_info_get_display_name(opener));
|
||||
|
||||
// It's documented that we can touch the child, if we want to use markup.
|
||||
#if 0
|
||||
GtkWidget *item = gtk_menu_item_new_with_label(name);
|
||||
#else
|
||||
// GtkImageMenuItem overrides the toggle_size_request class method
|
||||
// to get the image shown in the "margin"--too much work to duplicate.
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
|
||||
|
||||
GtkWidget *item = gtk_image_menu_item_new_with_label(name);
|
||||
GIcon *icon = g_app_info_get_icon(opener);
|
||||
if (icon) {
|
||||
GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU);
|
||||
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), image);
|
||||
gtk_image_menu_item_set_always_show_image(
|
||||
GTK_IMAGE_MENU_ITEM(item), TRUE);
|
||||
}
|
||||
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS;
|
||||
#endif
|
||||
|
||||
g_free(name);
|
||||
g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch),
|
||||
ctx, open_context_unref, 0);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
||||
}
|
||||
|
||||
static void
|
||||
on_chooser_activate(GtkMenuItem *item, gpointer user_data)
|
||||
{
|
||||
OpenContext *ctx = user_data;
|
||||
GtkWindow *window = g_weak_ref_get(&ctx->window);
|
||||
GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window,
|
||||
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type);
|
||||
g_clear_object(&window);
|
||||
|
||||
#if 0
|
||||
// This exists as a concept in mimeapps.list, but GNOME infuriatingly
|
||||
// infers it from the last used application if missing.
|
||||
gtk_app_chooser_widget_set_show_default(
|
||||
GTK_APP_CHOOSER_WIDGET(gtk_app_chooser_dialog_get_widget(
|
||||
GTK_APP_CHOOSER_DIALOG(dialog))), TRUE);
|
||||
#endif
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
|
||||
ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog));
|
||||
open_context_launch(GTK_WIDGET(item), ctx);
|
||||
}
|
||||
gtk_widget_destroy(dialog);
|
||||
}
|
||||
|
||||
static void
|
||||
on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
|
||||
{
|
||||
OpenContext *ctx = user_data;
|
||||
GtkWindow *window = g_weak_ref_get(&ctx->window);
|
||||
gchar *uri = g_file_get_uri(ctx->file);
|
||||
fiv_context_menu_information(window, uri);
|
||||
g_clear_object(&window);
|
||||
g_free(uri);
|
||||
}
|
||||
|
||||
void
|
||||
fiv_context_menu_remove(GtkWindow *parent, GFile *file)
|
||||
{
|
||||
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
|
||||
GError *error = NULL;
|
||||
if (!g_file_trash(file, NULL, &error))
|
||||
show_error_dialog(parent, error);
|
||||
}
|
||||
|
||||
static void
|
||||
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
|
||||
{
|
||||
OpenContext *ctx = user_data;
|
||||
GtkWindow *window = g_weak_ref_get(&ctx->window);
|
||||
fiv_context_menu_remove(window, ctx->file);
|
||||
g_clear_object(&window);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
destroy_widget_idle_source_func(GtkWidget *widget)
|
||||
{
|
||||
// The whole menu is deactivated /before/ any item is activated,
|
||||
// and a destroyed child item will not activate.
|
||||
gtk_widget_destroy(widget);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
GtkMenu *
|
||||
fiv_context_menu_new(GtkWidget *widget, GFile *file)
|
||||
{
|
||||
GFileInfo *info = g_file_query_info(file,
|
||||
G_FILE_ATTRIBUTE_STANDARD_TYPE
|
||||
"," G_FILE_ATTRIBUTE_STANDARD_NAME
|
||||
"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE
|
||||
"," G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
|
||||
G_FILE_QUERY_INFO_NONE, NULL, NULL);
|
||||
if (!info)
|
||||
return NULL;
|
||||
|
||||
GtkWindow *window = NULL;
|
||||
if (widget && GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget))))
|
||||
window = GTK_WINDOW(widget);
|
||||
|
||||
// This will have no application pre-assigned, for use with GTK+'s dialog.
|
||||
OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
|
||||
g_weak_ref_init(&ctx->window, window);
|
||||
if (!(ctx->content_type = g_strdup(g_file_info_get_content_type(info))))
|
||||
ctx->content_type = g_content_type_guess(NULL, NULL, 0, NULL);
|
||||
|
||||
GFileType type = g_file_info_get_file_type(info);
|
||||
const char *target_uri = g_file_info_get_attribute_string(
|
||||
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
|
||||
ctx->file = target_uri && g_file_has_uri_scheme(file, FIV_COLLECTION_SCHEME)
|
||||
? g_file_new_for_uri(target_uri)
|
||||
: g_object_ref(file);
|
||||
g_object_unref(info);
|
||||
|
||||
GAppInfo *default_ =
|
||||
g_app_info_get_default_for_type(ctx->content_type, FALSE);
|
||||
GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
|
||||
GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
|
||||
|
||||
GtkWidget *menu = gtk_menu_new();
|
||||
if (default_) {
|
||||
append_opener(menu, default_, ctx);
|
||||
gtk_menu_shell_append(
|
||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||
}
|
||||
|
||||
for (GList *iter = recommended; iter; iter = iter->next) {
|
||||
if (!default_ || !g_app_info_equal(iter->data, default_))
|
||||
append_opener(menu, iter->data, ctx);
|
||||
else
|
||||
g_object_unref(iter->data);
|
||||
}
|
||||
if (recommended) {
|
||||
g_list_free(recommended);
|
||||
gtk_menu_shell_append(
|
||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||
}
|
||||
|
||||
for (GList *iter = fallback; iter; iter = iter->next) {
|
||||
if (!default_ || !g_app_info_equal(iter->data, default_))
|
||||
append_opener(menu, iter->data, ctx);
|
||||
else
|
||||
g_object_unref(iter->data);
|
||||
}
|
||||
if (fallback) {
|
||||
g_list_free(fallback);
|
||||
gtk_menu_shell_append(
|
||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||
}
|
||||
|
||||
GtkWidget *item = gtk_menu_item_new_with_label("Open With...");
|
||||
g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate),
|
||||
ctx, open_context_unref, 0);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
||||
|
||||
// TODO(p): Can we avoid using the "trash" string constant for this check?
|
||||
if (!g_file_has_uri_scheme(file, "trash")) {
|
||||
gtk_menu_shell_append(
|
||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||
|
||||
item = gtk_menu_item_new_with_mnemonic("Move to _Trash");
|
||||
g_signal_connect_data(item, "activate", G_CALLBACK(on_trash_activate),
|
||||
g_rc_box_acquire(ctx), open_context_unref, 0);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
||||
}
|
||||
if (type == G_FILE_TYPE_REGULAR) {
|
||||
gtk_menu_shell_append(
|
||||
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||
|
||||
item = gtk_menu_item_new_with_mnemonic("_Information");
|
||||
g_signal_connect_data(item, "activate", G_CALLBACK(on_info_activate),
|
||||
g_rc_box_acquire(ctx), open_context_unref, 0);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
||||
}
|
||||
|
||||
// As per GTK+ 3 Common Questions, 1.5.
|
||||
g_object_ref_sink(menu);
|
||||
g_signal_connect_swapped(menu, "deactivate",
|
||||
G_CALLBACK(g_idle_add), destroy_widget_idle_source_func);
|
||||
g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL);
|
||||
|
||||
gtk_widget_show_all(menu);
|
||||
return GTK_MENU(menu);
|
||||
}
|
||||
22
fiv-context-menu.h
Normal file
22
fiv-context-menu.h
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// fiv-context-menu.h: popup menu
|
||||
//
|
||||
// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
|
||||
void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
|
||||
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);
|
||||
463
fiv-io-cmm.c
Normal file
463
fiv-io-cmm.c
Normal file
@@ -0,0 +1,463 @@
|
||||
//
|
||||
// fiv-io-cmm.c: colour management
|
||||
//
|
||||
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "fiv-io.h"
|
||||
|
||||
// Colour management must be handled before RGB conversions.
|
||||
// TODO(p): Make it also possible to use Skia's skcms.
|
||||
#ifdef HAVE_LCMS2
|
||||
#include <lcms2.h>
|
||||
#endif // HAVE_LCMS2
|
||||
#ifdef HAVE_LCMS2_FAST_FLOAT
|
||||
#include <lcms2_fast_float.h>
|
||||
#endif // HAVE_LCMS2_FAST_FLOAT
|
||||
|
||||
// --- CMM-independent transforms ----------------------------------------------
|
||||
|
||||
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
|
||||
// ARGB/BGRA/XRGB/BGRX.
|
||||
static void
|
||||
trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
|
||||
{
|
||||
// This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
|
||||
// It will typically produce horribly oversaturated results.
|
||||
// Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop
|
||||
// does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096
|
||||
while (len--) {
|
||||
int c = p[0], m = p[1], y = p[2], k = p[3];
|
||||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||||
p[0] = k * y / 255;
|
||||
p[1] = k * m / 255;
|
||||
p[2] = k * c / 255;
|
||||
p[3] = 255;
|
||||
#else
|
||||
p[3] = k * y / 255;
|
||||
p[2] = k * m / 255;
|
||||
p[1] = k * c / 255;
|
||||
p[0] = 255;
|
||||
#endif
|
||||
p += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// From libwebp, verified to exactly match [x * a / 255].
|
||||
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
|
||||
|
||||
void
|
||||
fiv_io_premultiply_argb32(FivIoImage *image)
|
||||
{
|
||||
if (image->format != CAIRO_FORMAT_ARGB32)
|
||||
return;
|
||||
|
||||
for (uint32_t y = 0; y < image->height; y++) {
|
||||
uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
|
||||
for (uint32_t x = 0; x < image->width; x++) {
|
||||
uint32_t argb = dstp[x], a = argb >> 24;
|
||||
dstp[x] = a << 24 |
|
||||
PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
|
||||
PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 |
|
||||
PREMULTIPLY8(a, 0xFF & argb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Profiles ----------------------------------------------------------------
|
||||
#ifdef HAVE_LCMS2
|
||||
|
||||
struct _FivIoProfile {
|
||||
FivIoCmm *cmm;
|
||||
cmsHPROFILE profile;
|
||||
};
|
||||
|
||||
GBytes *
|
||||
fiv_io_profile_to_bytes(FivIoProfile *profile)
|
||||
{
|
||||
cmsUInt32Number len = 0;
|
||||
(void) cmsSaveProfileToMem(profile, NULL, &len);
|
||||
gchar *data = g_malloc0(len);
|
||||
if (!cmsSaveProfileToMem(profile, data, &len)) {
|
||||
g_free(data);
|
||||
return NULL;
|
||||
}
|
||||
return g_bytes_new_take(data, len);
|
||||
}
|
||||
|
||||
static FivIoProfile *
|
||||
fiv_io_profile_new(FivIoCmm *cmm, cmsHPROFILE profile)
|
||||
{
|
||||
FivIoProfile *self = g_new0(FivIoProfile, 1);
|
||||
self->cmm = g_object_ref(cmm);
|
||||
self->profile = profile;
|
||||
return self;
|
||||
}
|
||||
|
||||
void
|
||||
fiv_io_profile_free(FivIoProfile *self)
|
||||
{
|
||||
cmsCloseProfile(self->profile);
|
||||
g_clear_object(&self->cmm);
|
||||
g_free(self);
|
||||
}
|
||||
|
||||
#else // ! HAVE_LCMS2
|
||||
|
||||
GBytes *fiv_io_profile_to_bytes(FivIoProfile *) { return NULL; }
|
||||
void fiv_io_profile_free(FivIoProfile *) {}
|
||||
|
||||
#endif // ! HAVE_LCMS2
|
||||
// --- Contexts ----------------------------------------------------------------
|
||||
#ifdef HAVE_LCMS2
|
||||
|
||||
struct _FivIoCmm {
|
||||
GObject parent_instance;
|
||||
cmsContext context;
|
||||
|
||||
// https://github.com/mm2/Little-CMS/issues/430
|
||||
gboolean broken_premul;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FivIoCmm, fiv_io_cmm, G_TYPE_OBJECT)
|
||||
|
||||
static void
|
||||
fiv_io_cmm_finalize(GObject *gobject)
|
||||
{
|
||||
FivIoCmm *self = FIV_IO_CMM(gobject);
|
||||
cmsDeleteContext(self->context);
|
||||
|
||||
G_OBJECT_CLASS(fiv_io_cmm_parent_class)->finalize(gobject);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_cmm_class_init(FivIoCmmClass *klass)
|
||||
{
|
||||
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
||||
object_class->finalize = fiv_io_cmm_finalize;
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_cmm_init(FivIoCmm *self)
|
||||
{
|
||||
self->context = cmsCreateContext(NULL, self);
|
||||
#ifdef HAVE_LCMS2_FAST_FLOAT
|
||||
if (cmsPluginTHR(self->context, cmsFastFloatExtensions()))
|
||||
self->broken_premul = LCMS_VERSION <= 2160;
|
||||
#endif // HAVE_LCMS2_FAST_FLOAT
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
FivIoCmm *
|
||||
fiv_io_cmm_get_default(void)
|
||||
{
|
||||
static gsize initialization_value = 0;
|
||||
static FivIoCmm *default_ = NULL;
|
||||
if (g_once_init_enter(&initialization_value)) {
|
||||
gsize setup_value = 1;
|
||||
default_ = g_object_new(FIV_TYPE_IO_CMM, NULL);
|
||||
g_once_init_leave(&initialization_value, setup_value);
|
||||
}
|
||||
return default_;
|
||||
}
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile(FivIoCmm *self, const void *data, size_t len)
|
||||
{
|
||||
g_return_val_if_fail(self != NULL, NULL);
|
||||
|
||||
return fiv_io_profile_new(self,
|
||||
cmsOpenProfileFromMemTHR(self->context, data, len));
|
||||
}
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile_sRGB(FivIoCmm *self)
|
||||
{
|
||||
g_return_val_if_fail(self != NULL, NULL);
|
||||
|
||||
return fiv_io_profile_new(self,
|
||||
cmsCreate_sRGBProfileTHR(self->context));
|
||||
}
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile_parametric(FivIoCmm *self,
|
||||
double gamma, double whitepoint[2], double primaries[6])
|
||||
{
|
||||
g_return_val_if_fail(self != NULL, NULL);
|
||||
|
||||
const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0};
|
||||
const cmsCIExyYTRIPLE cmsP = {
|
||||
{primaries[0], primaries[1], 1.0},
|
||||
{primaries[2], primaries[3], 1.0},
|
||||
{primaries[4], primaries[5], 1.0},
|
||||
};
|
||||
|
||||
cmsToneCurve *curve = cmsBuildGamma(self->context, gamma);
|
||||
if (!curve)
|
||||
return NULL;
|
||||
|
||||
cmsHPROFILE profile = cmsCreateRGBProfileTHR(self->context,
|
||||
&cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
|
||||
cmsFreeToneCurve(curve);
|
||||
return fiv_io_profile_new(self, profile);
|
||||
}
|
||||
|
||||
#else // ! HAVE_LCMS2
|
||||
|
||||
FivIoCmm *
|
||||
fiv_io_cmm_get_default()
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile(FivIoCmm *, const void *, size_t)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile_sRGB(FivIoCmm *)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile_parametric(FivIoCmm *, double, double[2], double[6])
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif // ! HAVE_LCMS2
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma)
|
||||
{
|
||||
return fiv_io_cmm_get_profile_parametric(self, gamma,
|
||||
(double[2]){0.3127, 0.3290},
|
||||
(double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
|
||||
}
|
||||
|
||||
FivIoProfile *
|
||||
fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes)
|
||||
{
|
||||
gsize len = 0;
|
||||
gconstpointer p = g_bytes_get_data(bytes, &len);
|
||||
return fiv_io_cmm_get_profile(self, p, len);
|
||||
}
|
||||
|
||||
// --- Image loading -----------------------------------------------------------
|
||||
#ifdef HAVE_LCMS2
|
||||
|
||||
// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
|
||||
#define FIV_IO_PROFILE_ARGB32 \
|
||||
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
|
||||
#define FIV_IO_PROFILE_4X16LE \
|
||||
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
|
||||
|
||||
void
|
||||
fiv_io_cmm_cmyk(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
|
||||
{
|
||||
g_return_if_fail(target == NULL || self != NULL);
|
||||
|
||||
cmsHTRANSFORM transform = NULL;
|
||||
if (source && target) {
|
||||
transform = cmsCreateTransformTHR(self->context,
|
||||
source->profile, TYPE_CMYK_8_REV,
|
||||
target->profile, FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
|
||||
}
|
||||
if (transform) {
|
||||
cmsDoTransform(
|
||||
transform, image->data, image->data, image->width * image->height);
|
||||
cmsDeleteTransform(transform);
|
||||
return;
|
||||
}
|
||||
trivial_cmyk_to_host_byte_order_argb(
|
||||
image->data, image->width * image->height);
|
||||
}
|
||||
|
||||
static bool
|
||||
fiv_io_cmm_rgb_direct(FivIoCmm *self, unsigned char *data, int w, int h,
|
||||
FivIoProfile *source, FivIoProfile *target,
|
||||
uint32_t source_format, uint32_t target_format)
|
||||
{
|
||||
g_return_val_if_fail(target == NULL || self != NULL, false);
|
||||
|
||||
// TODO(p): We should make this optional.
|
||||
FivIoProfile *src_fallback = NULL;
|
||||
if (target && !source)
|
||||
source = src_fallback = fiv_io_cmm_get_profile_sRGB(self);
|
||||
|
||||
cmsHTRANSFORM transform = NULL;
|
||||
if (source && target) {
|
||||
transform = cmsCreateTransformTHR(self->context,
|
||||
source->profile, source_format,
|
||||
target->profile, target_format, INTENT_PERCEPTUAL, 0);
|
||||
}
|
||||
if (transform) {
|
||||
cmsDoTransform(transform, data, data, w * h);
|
||||
cmsDeleteTransform(transform);
|
||||
}
|
||||
if (src_fallback)
|
||||
fiv_io_profile_free(src_fallback);
|
||||
return transform != NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_cmm_xrgb32(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
|
||||
{
|
||||
fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
|
||||
source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
|
||||
}
|
||||
|
||||
void
|
||||
fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
|
||||
int w, int h, FivIoProfile *source, FivIoProfile *target)
|
||||
{
|
||||
fiv_io_cmm_rgb_direct(self, data, w, h, source, target,
|
||||
FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
|
||||
}
|
||||
|
||||
#else // ! HAVE_LCMS2
|
||||
|
||||
void
|
||||
fiv_io_cmm_cmyk(FivIoCmm *, FivIoImage *image, FivIoProfile *, FivIoProfile *)
|
||||
{
|
||||
trivial_cmyk_to_host_byte_order_argb(
|
||||
image->data, image->width * image->height);
|
||||
}
|
||||
|
||||
static void
|
||||
fiv_io_cmm_xrgb32(FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
fiv_io_cmm_4x16le_direct(
|
||||
FivIoCmm *, unsigned char *, int, int, FivIoProfile *, FivIoProfile *)
|
||||
{
|
||||
}
|
||||
|
||||
#endif // ! HAVE_LCMS2
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
|
||||
|
||||
#define FIV_IO_PROFILE_ARGB32_PREMUL \
|
||||
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
|
||||
|
||||
static void
|
||||
fiv_io_cmm_argb32(FivIoCmm *self, FivIoImage *image,
|
||||
FivIoProfile *source, FivIoProfile *target)
|
||||
{
|
||||
g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
|
||||
|
||||
// TODO: With self->broken_premul,
|
||||
// this probably also needs to be wrapped in un-premultiplication.
|
||||
fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
|
||||
source, target,
|
||||
FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
|
||||
}
|
||||
|
||||
void
|
||||
fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
|
||||
{
|
||||
g_return_if_fail(target == NULL || self != NULL);
|
||||
|
||||
if (image->format != CAIRO_FORMAT_ARGB32) {
|
||||
fiv_io_cmm_xrgb32(self, image, source, target);
|
||||
} else if (!target || self->broken_premul) {
|
||||
fiv_io_cmm_xrgb32(self, image, source, target);
|
||||
fiv_io_premultiply_argb32(image);
|
||||
} else if (!fiv_io_cmm_rgb_direct(self, image->data,
|
||||
image->width, image->height, source, target,
|
||||
FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
|
||||
g_debug("failed to create a premultiplying transform");
|
||||
fiv_io_premultiply_argb32(image);
|
||||
}
|
||||
}
|
||||
|
||||
#else // ! HAVE_LCMS2 || LCMS_VERSION < 2130
|
||||
|
||||
static void
|
||||
fiv_io_cmm_argb32(G_GNUC_UNUSED FivIoCmm *self, G_GNUC_UNUSED FivIoImage *image,
|
||||
G_GNUC_UNUSED FivIoProfile *source, G_GNUC_UNUSED FivIoProfile *target)
|
||||
{
|
||||
// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
|
||||
}
|
||||
|
||||
void
|
||||
fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
|
||||
{
|
||||
fiv_io_cmm_xrgb32(self, image, source, target);
|
||||
fiv_io_premultiply_argb32(image);
|
||||
}
|
||||
|
||||
#endif // ! HAVE_LCMS2 || LCMS_VERSION < 2130
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
void
|
||||
fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
|
||||
void (*frame_cb) (FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *))
|
||||
{
|
||||
FivIoProfile *source = NULL;
|
||||
if (page->icc)
|
||||
source = fiv_io_cmm_get_profile_from_bytes(self, page->icc);
|
||||
|
||||
// TODO(p): All animations need to be composited in a linear colour space.
|
||||
for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
|
||||
frame_cb(self, frame, source, target);
|
||||
|
||||
if (source)
|
||||
fiv_io_profile_free(source);
|
||||
}
|
||||
|
||||
void
|
||||
fiv_io_cmm_any(FivIoCmm *self,
|
||||
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
|
||||
{
|
||||
// TODO(p): Ensure we do colour management early enough, so that
|
||||
// no avoidable increase of quantization error occurs beforehands,
|
||||
// and also for correct alpha compositing.
|
||||
switch (image->format) {
|
||||
break; case CAIRO_FORMAT_RGB24:
|
||||
fiv_io_cmm_xrgb32(self, image, source, target);
|
||||
break; case CAIRO_FORMAT_ARGB32:
|
||||
fiv_io_cmm_argb32(self, image, source, target);
|
||||
break; default:
|
||||
g_debug("CM attempted on an unsupported surface format");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
|
||||
FivIoImage *
|
||||
fiv_io_cmm_finish(FivIoCmm *self, FivIoImage *image, FivIoProfile *target)
|
||||
{
|
||||
if (!target)
|
||||
return image;
|
||||
|
||||
for (FivIoImage *page = image; page != NULL; page = page->page_next)
|
||||
fiv_io_cmm_page(self, page, target, fiv_io_cmm_any);
|
||||
return image;
|
||||
}
|
||||
743
fiv-io-model.c
Normal file
743
fiv-io-model.c
Normal 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
72
fiv-io-model.h
Normal 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);
|
||||
283
fiv-io.h
283
fiv-io.h
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-io.h: image operations
|
||||
//
|
||||
// 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.
|
||||
@@ -22,125 +22,62 @@
|
||||
#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 will support premultiplied alpha in 2022.
|
||||
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 -----------------------------------------------------------------
|
||||
|
||||
extern const char *fiv_io_supported_media_types[];
|
||||
|
||||
char **fiv_io_all_supported_media_types(void);
|
||||
|
||||
// Userdata are typically attached to all Cairo surfaces in an animation.
|
||||
|
||||
/// 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;
|
||||
|
||||
/// The next frame in a sequence, as a surface, in a chain, pre-composited.
|
||||
/// There is no wrap-around.
|
||||
extern cairo_user_data_key_t fiv_io_key_frame_next;
|
||||
/// The previous frame in a sequence, as a surface, in a chain, pre-composited.
|
||||
/// This is a weak pointer that wraps around, and needn't be present
|
||||
/// for static images.
|
||||
extern cairo_user_data_key_t fiv_io_key_frame_previous;
|
||||
/// Frame duration in milliseconds as an intptr_t.
|
||||
extern cairo_user_data_key_t fiv_io_key_frame_duration;
|
||||
/// How many times to repeat the animation, or zero for +inf, as a uintptr_t.
|
||||
extern cairo_user_data_key_t fiv_io_key_loops;
|
||||
|
||||
/// The first frame of the next page, as a surface, in a chain.
|
||||
/// There is no wrap-around.
|
||||
extern cairo_user_data_key_t fiv_io_key_page_next;
|
||||
/// The first frame of the previous page, as a surface, in a chain.
|
||||
/// There is no wrap-around. This is a weak pointer.
|
||||
extern cairo_user_data_key_t fiv_io_key_page_previous;
|
||||
|
||||
typedef struct _FivIoRenderClosure {
|
||||
/// The rendering is allowed to fail, returning NULL.
|
||||
cairo_surface_t *(*render)(struct _FivIoRenderClosure *, double scale);
|
||||
} 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;
|
||||
|
||||
typedef struct {
|
||||
const char *uri; ///< Source URI
|
||||
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(
|
||||
const char *data, size_t len, const FivIoOpenContext *ctx, GError **error);
|
||||
|
||||
// --- Thumbnail passing utilities ---------------------------------------------
|
||||
|
||||
void fiv_io_serialize_to_stdout(cairo_surface_t *surface);
|
||||
cairo_surface_t *fiv_io_deserialize(GBytes *bytes);
|
||||
|
||||
// --- 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);
|
||||
|
||||
GPtrArray *fiv_io_model_get_files(FivIoModel *self);
|
||||
GPtrArray *fiv_io_model_get_subdirectories(FivIoModel *self);
|
||||
|
||||
// --- Export ------------------------------------------------------------------
|
||||
|
||||
/// Encodes a Cairo surface 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);
|
||||
|
||||
/// 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 gchar *path, GError **error);
|
||||
|
||||
// --- Metadata ----------------------------------------------------------------
|
||||
gchar **fiv_io_all_supported_media_types(void);
|
||||
|
||||
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
|
||||
typedef enum _FivIoOrientation {
|
||||
enum _FivIoOrientation {
|
||||
FivIoOrientationUnknown = 0,
|
||||
FivIoOrientation0 = 1,
|
||||
FivIoOrientationMirror0 = 2,
|
||||
@@ -150,12 +87,107 @@ typedef enum _FivIoOrientation {
|
||||
FivIoOrientation90 = 6,
|
||||
FivIoOrientationMirror90 = 7,
|
||||
FivIoOrientation270 = 8
|
||||
} FivIoOrientation;
|
||||
};
|
||||
|
||||
/// Returns a rendering matrix for a surface, and its target dimensions.
|
||||
cairo_matrix_t fiv_io_orientation_apply(cairo_surface_t *surface,
|
||||
// TODO(p): Maybe make FivIoProfile a referencable type,
|
||||
// then loaders could store it in their closures.
|
||||
struct _FivIoRenderClosure {
|
||||
/// The rendering is allowed to fail, returning NULL.
|
||||
FivIoImage *(*render)(
|
||||
FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
|
||||
void (*destroy)(FivIoRenderClosure *);
|
||||
};
|
||||
|
||||
// Metadata are typically attached to all Cairo surfaces in an animation.
|
||||
|
||||
struct _FivIoImage {
|
||||
uint8_t *data; ///< Raw image data
|
||||
cairo_format_t format; ///< Data format
|
||||
uint32_t width; ///< Width of the image in pixels
|
||||
uint32_t stride; ///< Row stride in bytes
|
||||
uint32_t height; ///< Height of the image in pixels
|
||||
|
||||
FivIoOrientation orientation; ///< Orientation to use for display
|
||||
|
||||
GBytes *exif; ///< Raw Exif/TIFF segment
|
||||
GBytes *icc; ///< Raw ICC profile data
|
||||
GBytes *xmp; ///< Raw XMP data
|
||||
GBytes *thum; ///< WebP THUM chunk, for our thumbnails
|
||||
|
||||
/// GHashTable with key-value pairs from PNG's tEXt, zTXt, iTXt chunks.
|
||||
/// Currently only read by fiv_io_open_png_thumbnail().
|
||||
GHashTable *text;
|
||||
|
||||
/// A FivIoRenderClosure for parametrized re-rendering of vector formats.
|
||||
/// This is attached at the page level.
|
||||
FivIoRenderClosure *render;
|
||||
|
||||
/// The first frame of the next page, in a chain.
|
||||
/// There is no wrap-around.
|
||||
FivIoImage *page_next;
|
||||
|
||||
/// The first frame of the previous page, in a chain.
|
||||
/// There is no wrap-around. This is a weak pointer.
|
||||
FivIoImage *page_previous;
|
||||
|
||||
/// The next frame in a sequence, in a chain, pre-composited.
|
||||
/// There is no wrap-around.
|
||||
FivIoImage *frame_next;
|
||||
|
||||
/// The previous frame in a sequence, in a chain, pre-composited.
|
||||
/// This is a weak pointer that wraps around,
|
||||
/// and needn't be present for static images.
|
||||
FivIoImage *frame_previous;
|
||||
|
||||
/// Frame duration in milliseconds.
|
||||
int64_t frame_duration;
|
||||
|
||||
/// How many times to repeat the animation, or zero for +inf.
|
||||
uint64_t loops;
|
||||
};
|
||||
|
||||
FivIoImage *fiv_io_image_ref(FivIoImage *image);
|
||||
void fiv_io_image_unref(FivIoImage *image);
|
||||
|
||||
/// Analogous to cairo_image_surface_create(). May return NULL.
|
||||
FivIoImage *fiv_io_image_new(
|
||||
cairo_format_t format, uint32_t width, uint32_t height);
|
||||
|
||||
/// Return a new Cairo image surface referencing the same data as the image,
|
||||
/// eating the reference to it.
|
||||
cairo_surface_t *fiv_io_image_to_surface(FivIoImage *image);
|
||||
|
||||
/// Return a new Cairo image surface referencing the same data as the image,
|
||||
/// without eating the image's reference.
|
||||
cairo_surface_t *fiv_io_image_to_surface_noref(const FivIoImage *image);
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
typedef struct {
|
||||
const char *uri; ///< Source URI
|
||||
FivIoCmm *cmm; ///< Colour management module or NULL
|
||||
FivIoProfile *screen_profile; ///< Target colour space or NULL
|
||||
int screen_dpi; ///< Target DPI
|
||||
gboolean enhance; ///< Enhance JPEG (currently)
|
||||
gboolean first_frame_only; ///< Only interested in the 1st frame
|
||||
GPtrArray *warnings; ///< String vector for non-fatal errors
|
||||
} FivIoOpenContext;
|
||||
|
||||
FivIoImage *fiv_io_open(const FivIoOpenContext *ctx, GError **error);
|
||||
FivIoImage *fiv_io_open_from_data(
|
||||
const char *data, size_t len, const FivIoOpenContext *ctx, GError **error);
|
||||
|
||||
FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error);
|
||||
|
||||
// --- Metadata ----------------------------------------------------------------
|
||||
|
||||
/// Returns a rendering matrix for an image (user space to pattern space),
|
||||
/// and its target dimensions.
|
||||
cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image,
|
||||
FivIoOrientation orientation, double *width, double *height);
|
||||
void fiv_io_orientation_dimensions(cairo_surface_t *surface,
|
||||
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.
|
||||
@@ -163,4 +195,25 @@ 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 gchar *path, GError **error);
|
||||
const FivIoImage *page, const char *path, GError **error);
|
||||
|
||||
// --- Thumbnail passing utilities ---------------------------------------------
|
||||
|
||||
enum { FIV_IO_SERIALIZE_LOW_QUALITY = 1 << 0 };
|
||||
|
||||
void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
|
||||
cairo_surface_t *fiv_io_deserialize(GBytes *bytes, guint64 *user_data);
|
||||
|
||||
GBytes *fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error);
|
||||
|
||||
// --- Export ------------------------------------------------------------------
|
||||
|
||||
/// Encodes an image as a WebP bitstream, following the configuration.
|
||||
/// The result needs to be freed using WebPFree/WebPDataClear().
|
||||
unsigned char *fiv_io_encode_webp(
|
||||
FivIoImage *image, const WebPConfig *config, size_t *len);
|
||||
|
||||
/// Saves the page as a lossless WebP still picture or animation.
|
||||
/// If no exact frame is specified, this potentially creates an animation.
|
||||
gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
|
||||
FivIoProfile *target, const char *path, GError **error);
|
||||
|
||||
137
fiv-jpegcrop.c
137
fiv-jpegcrop.c
@@ -18,14 +18,17 @@
|
||||
#include <gtk/gtk.h>
|
||||
#include <turbojpeg.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
// --- Utilities ---------------------------------------------------------------
|
||||
|
||||
static void exit_fatal(const gchar *format, ...) G_GNUC_PRINTF(1, 2);
|
||||
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
|
||||
|
||||
static void
|
||||
exit_fatal(const gchar *format, ...)
|
||||
exit_fatal(const char *format, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, format);
|
||||
@@ -96,16 +99,10 @@ on_draw(G_GNUC_UNUSED GtkWidget *widget, cairo_t *cr,
|
||||
}
|
||||
|
||||
static GFile *
|
||||
choose_filename(void)
|
||||
run_chooser(GtkWidget *dialog)
|
||||
{
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new("Saved cropped image as",
|
||||
GTK_WINDOW(g.window), GTK_FILE_CHOOSER_ACTION_SAVE,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Save", GTK_RESPONSE_ACCEPT, NULL);
|
||||
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
|
||||
gtk_file_chooser_set_local_only(chooser, FALSE);
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
|
||||
(void) gtk_file_chooser_set_file(chooser, g.location, NULL);
|
||||
|
||||
GtkFileFilter *jpeg = gtk_file_filter_new();
|
||||
gtk_file_filter_add_mime_type(jpeg, "image/jpeg");
|
||||
@@ -134,6 +131,30 @@ choose_filename(void)
|
||||
}
|
||||
}
|
||||
|
||||
static GFile *
|
||||
choose_file_to_open(void)
|
||||
{
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new("Open image",
|
||||
NULL, GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Open", GTK_RESPONSE_ACCEPT, NULL);
|
||||
return run_chooser(dialog);
|
||||
}
|
||||
|
||||
static GFile *
|
||||
choose_file_to_save(void)
|
||||
{
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new("Saved cropped image as",
|
||||
GTK_WINDOW(g.window), GTK_FILE_CHOOSER_ACTION_SAVE,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Save", GTK_RESPONSE_ACCEPT, NULL);
|
||||
|
||||
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
|
||||
(void) gtk_file_chooser_set_file(chooser, g.location, NULL);
|
||||
return run_chooser(dialog);
|
||||
}
|
||||
|
||||
static void
|
||||
on_save_as(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
@@ -162,7 +183,7 @@ on_save_as(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
|
||||
goto out;
|
||||
}
|
||||
|
||||
GFile *file = choose_filename();
|
||||
GFile *file = choose_file_to_save();
|
||||
GError *error = NULL;
|
||||
if (file &&
|
||||
!g_file_replace_contents(file, (const char *) data, len, NULL, FALSE,
|
||||
@@ -249,8 +270,58 @@ on_motion(G_GNUC_UNUSED GtkWidget *self, GdkEventMotion *event,
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
on_drag_begin(
|
||||
GtkGestureDrag *self, gdouble start_x, gdouble start_y, gpointer user_data)
|
||||
{
|
||||
// The middle mouse button will never be triggered by touch screens,
|
||||
// so there is only the NULL sequence to care about.
|
||||
gtk_gesture_set_state(GTK_GESTURE(self), GTK_EVENT_SEQUENCE_CLAIMED);
|
||||
|
||||
GdkWindow *window = gtk_widget_get_window(g.view);
|
||||
GdkCursor *cursor =
|
||||
gdk_cursor_new_from_name(gdk_window_get_display(window), "grabbing");
|
||||
gdk_window_set_cursor(window, cursor);
|
||||
g_object_unref(cursor);
|
||||
|
||||
double *last = user_data;
|
||||
last[0] = start_x;
|
||||
last[1] = start_y;
|
||||
}
|
||||
|
||||
static void
|
||||
on_drag_update(GtkGestureDrag *self, gdouble offset_x, gdouble offset_y,
|
||||
gpointer user_data)
|
||||
{
|
||||
double start_x = 0, start_y = 0;
|
||||
gtk_gesture_drag_get_start_point(self, &start_x, &start_y);
|
||||
|
||||
double *last = user_data,
|
||||
diff_x = (start_x + offset_x) - last[0],
|
||||
diff_y = (start_y + offset_y) - last[1];
|
||||
|
||||
last[0] = start_x + offset_x;
|
||||
last[1] = start_y + offset_y;
|
||||
|
||||
GtkScrolledWindow *sw = GTK_SCROLLED_WINDOW(g.scrolled);
|
||||
GtkAdjustment *h = gtk_scrolled_window_get_hadjustment(sw);
|
||||
GtkAdjustment *v = gtk_scrolled_window_get_vadjustment(sw);
|
||||
if (diff_x)
|
||||
gtk_adjustment_set_value(h, gtk_adjustment_get_value(h) - diff_x);
|
||||
if (diff_y)
|
||||
gtk_adjustment_set_value(v, gtk_adjustment_get_value(v) - diff_y);
|
||||
}
|
||||
|
||||
static void
|
||||
on_drag_end(G_GNUC_UNUSED GtkGestureDrag *self, G_GNUC_UNUSED gdouble start_x,
|
||||
G_GNUC_UNUSED gdouble start_y, G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
// Cursors follow the widget hierarchy.
|
||||
gdk_window_set_cursor(gtk_widget_get_window(g.view), NULL);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
open_jpeg(const gchar *data, gsize len, GError **error)
|
||||
open_jpeg(const char *data, gsize len, GError **error)
|
||||
{
|
||||
tjhandle h = tjInitDecompress();
|
||||
if (!h) {
|
||||
@@ -315,10 +386,10 @@ int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
gboolean show_version = FALSE;
|
||||
gchar **path_args = NULL;
|
||||
gchar **args = NULL;
|
||||
|
||||
const GOptionEntry options[] = {
|
||||
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args,
|
||||
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
|
||||
NULL, "[FILE | URI]"},
|
||||
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
|
||||
&show_version, "Output version information and exit", NULL},
|
||||
@@ -329,21 +400,25 @@ main(int argc, char *argv[])
|
||||
gboolean initialized = gtk_init_with_args(
|
||||
&argc, &argv, " - Lossless JPEG cropper", options, NULL, &error);
|
||||
if (show_version) {
|
||||
printf("fiv-jpegcrop " PROJECT_VERSION "\n");
|
||||
const char *version = PROJECT_VERSION;
|
||||
printf("%s %s\n", "fiv-jpegcrop", &version[*version == 'v']);
|
||||
return 0;
|
||||
}
|
||||
if (!initialized)
|
||||
exit_fatal("%s", error->message);
|
||||
|
||||
gtk_window_set_default_icon_name(PROJECT_NAME);
|
||||
|
||||
// TODO(p): Rather use G_OPTION_ARG_CALLBACK with G_OPTION_FLAG_FILENAME.
|
||||
// Alternatively, GOptionContext with gtk_get_option_group(TRUE).
|
||||
// Then we can show the help string here instead (in fiv as well).
|
||||
if (!path_args || !path_args[0] || path_args[1])
|
||||
exit_fatal("invalid arguments");
|
||||
if (args && args[1])
|
||||
exit_fatal("Too many arguments");
|
||||
else if (args)
|
||||
g.location = g_file_new_for_commandline_arg(args[0]);
|
||||
else if (!(g.location = choose_file_to_open()))
|
||||
exit(EXIT_SUCCESS);
|
||||
|
||||
gtk_window_set_default_icon_name(PROJECT_NAME);
|
||||
|
||||
g.location = g_file_new_for_commandline_arg(path_args[0]);
|
||||
g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
g_signal_connect(g.window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
|
||||
|
||||
@@ -384,15 +459,16 @@ main(int argc, char *argv[])
|
||||
|
||||
g.view = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(g.view, g.width + 2, g.height + 2);
|
||||
gtk_widget_add_events(
|
||||
g.view, GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK);
|
||||
g_signal_connect(g.view, "draw", G_CALLBACK(on_draw), NULL);
|
||||
gtk_widget_add_events(g.view,
|
||||
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
|
||||
GDK_POINTER_MOTION_MASK);
|
||||
g_signal_connect(g.view, "draw",
|
||||
G_CALLBACK(on_draw), NULL);
|
||||
g_signal_connect(g.view, "button-press-event",
|
||||
G_CALLBACK(on_press), NULL);
|
||||
g_signal_connect(g.view, "motion-notify-event",
|
||||
G_CALLBACK(on_motion), NULL);
|
||||
|
||||
// TODO(p): Track middle mouse button drags, adjust the adjustments.
|
||||
g.scrolled = gtk_scrolled_window_new(NULL, NULL);
|
||||
gtk_scrolled_window_set_overlay_scrolling(
|
||||
GTK_SCROLLED_WINDOW(g.scrolled), FALSE);
|
||||
@@ -401,13 +477,26 @@ main(int argc, char *argv[])
|
||||
gtk_scrolled_window_set_propagate_natural_height(
|
||||
GTK_SCROLLED_WINDOW(g.scrolled), TRUE);
|
||||
|
||||
GtkGesture *drag = gtk_gesture_drag_new(g.scrolled);
|
||||
gtk_event_controller_set_propagation_phase(
|
||||
GTK_EVENT_CONTROLLER(drag), GTK_PHASE_CAPTURE);
|
||||
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_MIDDLE);
|
||||
|
||||
double last_drag_point[2] = {};
|
||||
g_signal_connect(drag, "drag-begin",
|
||||
G_CALLBACK(on_drag_begin), last_drag_point);
|
||||
g_signal_connect(drag, "drag-update",
|
||||
G_CALLBACK(on_drag_update), last_drag_point);
|
||||
g_signal_connect(drag, "drag-end",
|
||||
G_CALLBACK(on_drag_end), last_drag_point);
|
||||
|
||||
gtk_container_add(GTK_CONTAINER(g.scrolled), g.view);
|
||||
gtk_container_add(GTK_CONTAINER(g.window), g.scrolled);
|
||||
gtk_window_set_default_size(GTK_WINDOW(g.window), 800, 600);
|
||||
gtk_widget_show_all(g.window);
|
||||
|
||||
// It probably needs to be realized.
|
||||
GdkWindow *window = gtk_widget_get_window(g.view);
|
||||
GdkWindow *window = gtk_widget_get_window(g.scrolled);
|
||||
GdkCursor *cursor =
|
||||
gdk_cursor_new_from_name(gdk_window_get_display(window), "crosshair");
|
||||
gdk_window_set_cursor(window, cursor);
|
||||
|
||||
9
fiv-reverse-search
Executable file
9
fiv-reverse-search
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh -e
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 SEARCH-ENGINE-URI-PREFIX {PATH | URI}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
|
||||
| curl --silent --show-error --form 'files[]=@-' https://uguu.se/upload \
|
||||
| jq --raw-output '.files[] | .url | @uri')"
|
||||
10
fiv-reverse-search.desktop.in
Normal file
10
fiv-reverse-search.desktop.in
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=fiv @NAME@ Reverse Image Search
|
||||
GenericName=@NAME@ Reverse Image Search
|
||||
Icon=fiv
|
||||
Exec=fiv-reverse-search "@URL@" %u
|
||||
NoDisplay=true
|
||||
Terminal=false
|
||||
Categories=Graphics;2DGraphics;
|
||||
MimeType=image/png;image/bmp;image/gif;image/x-tga;image/jpeg;image/webp;
|
||||
210
fiv-sidebar.c
210
fiv-sidebar.c
@@ -17,19 +17,21 @@
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include "fiv-collection.h"
|
||||
#include "fiv-context-menu.h"
|
||||
#include "fiv-io.h"
|
||||
#include "fiv-sidebar.h"
|
||||
|
||||
struct _FivSidebar {
|
||||
GtkScrolledWindow parent_instance;
|
||||
GtkPlacesSidebar *places;
|
||||
GtkWidget *toolbar;
|
||||
GtkWidget *listbox;
|
||||
FivIoModel *model;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW)
|
||||
|
||||
G_DEFINE_QUARK(fiv-sidebar-drag-gesture-quark, fiv_sidebar_drag_gesture)
|
||||
G_DEFINE_QUARK(fiv-sidebar-location-quark, fiv_sidebar_location)
|
||||
G_DEFINE_QUARK(fiv-sidebar-self-quark, fiv_sidebar_self)
|
||||
|
||||
@@ -75,7 +77,7 @@ fiv_sidebar_class_init(FivSidebarClass *klass)
|
||||
|
||||
// You're giving me no choice, Adwaita.
|
||||
// Your style is hardcoded to match against the class' CSS name.
|
||||
// And I need replicate the internal widget structure.
|
||||
// And I need to replicate the internal widget structure.
|
||||
gtk_widget_class_set_css_name(widget_class, "placessidebar");
|
||||
|
||||
// TODO(p): Consider a return value, and using it.
|
||||
@@ -99,7 +101,7 @@ on_rowlabel_query_tooltip(GtkWidget *widget,
|
||||
}
|
||||
|
||||
static gboolean
|
||||
on_breadcrumb_release(
|
||||
on_breadcrumb_button_release(
|
||||
G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event, gpointer user_data)
|
||||
{
|
||||
// This also prevents unwanted primary button click handling in GtkListBox.
|
||||
@@ -119,6 +121,48 @@ on_breadcrumb_release(
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
on_breadcrumb_gesture_drag_begin(GtkGestureDrag *drag,
|
||||
G_GNUC_UNUSED gdouble start_x, G_GNUC_UNUSED gdouble start_y,
|
||||
G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
// Touch screen dragging is how you scroll the parent GtkScrolledWindow,
|
||||
// don't steal that gesture. Moreover, touch screen dragging fails
|
||||
// in the middle, without ever invoking drag-end.
|
||||
GtkGesture *gesture = GTK_GESTURE(drag);
|
||||
if (gdk_device_get_source(
|
||||
gdk_event_get_source_device(gtk_gesture_get_last_event(
|
||||
gesture, gtk_gesture_get_last_updated_sequence(gesture)))) ==
|
||||
GDK_SOURCE_TOUCHSCREEN)
|
||||
gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
|
||||
}
|
||||
|
||||
static void
|
||||
on_breadcrumb_gesture_drag_update(GtkGestureDrag *drag,
|
||||
gdouble offset_x, gdouble offset_y, gpointer user_data)
|
||||
{
|
||||
GtkWidget *widget = GTK_WIDGET(user_data);
|
||||
gdouble start_x = 0, start_y = 0;
|
||||
if (!gtk_gesture_drag_get_start_point(drag, &start_x, &start_y) ||
|
||||
!gtk_drag_check_threshold(widget, start_x, start_y,
|
||||
start_x + offset_x, start_y + offset_y))
|
||||
return;
|
||||
|
||||
GtkGesture *gesture = GTK_GESTURE(drag);
|
||||
gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_CLAIMED);
|
||||
|
||||
GdkEvent *event = gdk_event_copy(gtk_gesture_get_last_event(
|
||||
gesture, gtk_gesture_get_last_updated_sequence(gesture)));
|
||||
GtkTargetList *target_list = gtk_target_list_new(NULL, 0);
|
||||
gtk_target_list_add_uri_targets(target_list, 0);
|
||||
|
||||
gtk_drag_begin_with_coordinates(
|
||||
widget, target_list, GDK_ACTION_LINK, 1, event, start_x, start_y);
|
||||
|
||||
gtk_target_list_unref(target_list);
|
||||
gdk_event_free(event);
|
||||
}
|
||||
|
||||
static void
|
||||
on_breadcrumb_drag_data_get(G_GNUC_UNUSED GtkWidget *widget,
|
||||
G_GNUC_UNUSED GdkDragContext *context, GtkSelectionData *selection_data,
|
||||
@@ -137,6 +181,7 @@ static void
|
||||
on_breadcrumb_drag_begin(G_GNUC_UNUSED GtkWidget *widget,
|
||||
GdkDragContext *context, gpointer user_data)
|
||||
{
|
||||
gtk_drag_set_icon_name(context, "inode-directory-symbolic", 0, 0);
|
||||
gtk_places_sidebar_set_drop_targets_visible(user_data, TRUE, context);
|
||||
}
|
||||
|
||||
@@ -147,6 +192,29 @@ on_breadcrumb_drag_end(G_GNUC_UNUSED GtkWidget *widget,
|
||||
gtk_places_sidebar_set_drop_targets_visible(user_data, FALSE, context);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
on_breadcrumb_button_press(GtkWidget *widget, GdkEventButton *event,
|
||||
G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
if (!gdk_event_triggers_context_menu((const GdkEvent *) event))
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
|
||||
GFile *location =
|
||||
g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
|
||||
gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, location), NULL);
|
||||
return GDK_EVENT_STOP;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
on_breadcrumb_popup_menu(GtkWidget *widget, G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
GFile *location =
|
||||
g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
|
||||
gtk_menu_popup_at_widget(fiv_context_menu_new(widget, location), widget,
|
||||
GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
create_row(FivSidebar *self, GFile *file, const char *icon_name)
|
||||
{
|
||||
@@ -189,17 +257,31 @@ create_row(FivSidebar *self, GFile *file, const char *icon_name)
|
||||
GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_NONE);
|
||||
gtk_container_add(GTK_CONTAINER(revealer), rowbox);
|
||||
|
||||
gtk_drag_source_set(revealer, GDK_BUTTON1_MASK, NULL, 0, GDK_ACTION_LINK);
|
||||
gtk_drag_source_add_uri_targets(revealer);
|
||||
gtk_drag_source_set_icon_name(revealer, "inode-directory-symbolic");
|
||||
GtkGesture *drag = gtk_gesture_drag_new(revealer);
|
||||
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
|
||||
gtk_event_controller_set_propagation_phase(
|
||||
GTK_EVENT_CONTROLLER(drag), GTK_PHASE_BUBBLE);
|
||||
g_object_set_qdata_full(G_OBJECT(revealer),
|
||||
fiv_sidebar_drag_gesture_quark(),
|
||||
drag, (GDestroyNotify) g_object_unref);
|
||||
g_signal_connect(drag, "drag-begin",
|
||||
G_CALLBACK(on_breadcrumb_gesture_drag_begin), revealer);
|
||||
g_signal_connect(drag, "drag-update",
|
||||
G_CALLBACK(on_breadcrumb_gesture_drag_update), revealer);
|
||||
|
||||
GtkWidget *row = gtk_list_box_row_new();
|
||||
g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_location_quark(),
|
||||
g_object_ref(file), (GDestroyNotify) g_object_unref);
|
||||
g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_self_quark(),
|
||||
g_object_ref(self), (GDestroyNotify) g_object_unref);
|
||||
g_signal_connect(row, "button-press-event",
|
||||
G_CALLBACK(on_breadcrumb_button_press), NULL);
|
||||
g_signal_connect(row, "popup-menu",
|
||||
G_CALLBACK(on_breadcrumb_popup_menu), NULL);
|
||||
|
||||
// Drag signals need to be hooked to a widget with its own GdkWindow.
|
||||
g_signal_connect(revealer, "button-release-event",
|
||||
G_CALLBACK(on_breadcrumb_release), row);
|
||||
G_CALLBACK(on_breadcrumb_button_release), row);
|
||||
g_signal_connect(revealer, "drag-data-get",
|
||||
G_CALLBACK(on_breadcrumb_drag_data_get), row);
|
||||
g_signal_connect(revealer, "drag-begin",
|
||||
@@ -209,19 +291,34 @@ create_row(FivSidebar *self, GFile *file, const char *icon_name)
|
||||
|
||||
gtk_container_add(GTK_CONTAINER(row), revealer);
|
||||
gtk_widget_show_all(row);
|
||||
g_object_unref(info);
|
||||
return row;
|
||||
}
|
||||
|
||||
static void
|
||||
update_location(FivSidebar *self)
|
||||
on_update_task(GTask *task, G_GNUC_UNUSED gpointer source_object,
|
||||
G_GNUC_UNUSED gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
|
||||
{
|
||||
g_task_return_boolean(task, TRUE);
|
||||
}
|
||||
|
||||
static void
|
||||
on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res,
|
||||
G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
FivSidebar *self = FIV_SIDEBAR(source_object);
|
||||
gtk_places_sidebar_set_location(
|
||||
self->places, fiv_io_model_get_location(self->model));
|
||||
}
|
||||
|
||||
static void
|
||||
reload_directories(FivSidebar *self)
|
||||
{
|
||||
GFile *location = fiv_io_model_get_location(self->model);
|
||||
if (!location)
|
||||
return;
|
||||
|
||||
gtk_places_sidebar_set_location(self->places, location);
|
||||
gtk_container_foreach(GTK_CONTAINER(self->listbox),
|
||||
(GtkCallback) gtk_widget_destroy, NULL);
|
||||
if (!location)
|
||||
return;
|
||||
|
||||
GFile *iter = g_object_ref(location);
|
||||
GtkWidget *row = NULL;
|
||||
@@ -240,14 +337,50 @@ update_location(FivSidebar *self)
|
||||
if ((row = create_row(self, location, "circle-filled-symbolic")))
|
||||
gtk_container_add(GTK_CONTAINER(self->listbox), row);
|
||||
|
||||
GPtrArray *subdirs = fiv_io_model_get_subdirectories(self->model);
|
||||
for (guint i = 0; i < subdirs->len; i++) {
|
||||
GFile *file = g_file_new_for_uri(subdirs->pdata[i]);
|
||||
gsize len = 0;
|
||||
FivIoModelEntry *const *subdirs =
|
||||
fiv_io_model_get_subdirs(self->model, &len);
|
||||
for (gsize i = 0; i < len; i++) {
|
||||
GFile *file = g_file_new_for_uri(subdirs[i]->uri);
|
||||
if ((row = create_row(self, file, "go-down-symbolic")))
|
||||
gtk_container_add(GTK_CONTAINER(self->listbox), row);
|
||||
g_object_unref(file);
|
||||
}
|
||||
g_ptr_array_free(subdirs, TRUE);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -300,13 +433,16 @@ 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;
|
||||
|
||||
char *parse_name = g_file_get_parse_name(child);
|
||||
gchar *parse_name = g_file_get_parse_name(child);
|
||||
if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) {
|
||||
char *save = parse_name;
|
||||
gchar *save = parse_name;
|
||||
parse_name = g_strdup_printf("%s%c", parse_name, G_DIR_SEPARATOR);
|
||||
g_free(save);
|
||||
}
|
||||
@@ -401,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);
|
||||
@@ -446,7 +589,8 @@ fiv_sidebar_init(FivSidebar *self)
|
||||
// TODO(p): Transplant functionality from the shitty GtkPlacesSidebar.
|
||||
// We cannot reasonably place any new items within its own GtkListBox,
|
||||
// so we need to replicate the style hierarchy to some extent.
|
||||
self->places = GTK_PLACES_SIDEBAR(gtk_places_sidebar_new());
|
||||
GtkWidget *places = gtk_places_sidebar_new();
|
||||
self->places = GTK_PLACES_SIDEBAR(places);
|
||||
gtk_places_sidebar_set_show_recent(self->places, FALSE);
|
||||
gtk_places_sidebar_set_show_trash(self->places, FALSE);
|
||||
gtk_places_sidebar_set_open_flags(self->places,
|
||||
@@ -454,18 +598,16 @@ fiv_sidebar_init(FivSidebar *self)
|
||||
g_signal_connect(self->places, "open-location",
|
||||
G_CALLBACK(on_open_location), self);
|
||||
|
||||
gint minimum_width = -1;
|
||||
gtk_widget_get_size_request(places, &minimum_width, NULL);
|
||||
gtk_widget_set_size_request(places, minimum_width, -1);
|
||||
|
||||
gtk_places_sidebar_set_show_enter_location(self->places, TRUE);
|
||||
g_signal_connect(self->places, "show-enter-location",
|
||||
G_CALLBACK(on_show_enter_location), self);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->places),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_NEVER);
|
||||
|
||||
// None of GtkActionBar, GtkToolbar, .inline-toolbar is appropriate.
|
||||
// It is either side-favouring borders or excess button padding.
|
||||
self->toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
||||
gtk_style_context_add_class(
|
||||
gtk_widget_get_style_context(self->toolbar), GTK_STYLE_CLASS_TOOLBAR);
|
||||
|
||||
self->listbox = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(
|
||||
GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);
|
||||
@@ -479,10 +621,6 @@ fiv_sidebar_init(FivSidebar *self)
|
||||
GTK_CONTAINER(superbox), GTK_WIDGET(self->places));
|
||||
gtk_container_add(
|
||||
GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
|
||||
gtk_container_add(
|
||||
GTK_CONTAINER(superbox), self->toolbar);
|
||||
gtk_container_add(
|
||||
GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
|
||||
gtk_container_add(
|
||||
GTK_CONTAINER(superbox), self->listbox);
|
||||
gtk_container_add(GTK_CONTAINER(self), superbox);
|
||||
@@ -511,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);
|
||||
}
|
||||
@@ -525,10 +664,3 @@ fiv_sidebar_show_enter_location(FivSidebar *self)
|
||||
g_return_if_fail(FIV_IS_SIDEBAR(self));
|
||||
g_signal_emit_by_name(self->places, "show-enter-location");
|
||||
}
|
||||
|
||||
GtkBox *
|
||||
fiv_sidebar_get_toolbar(FivSidebar *self)
|
||||
{
|
||||
g_return_val_if_fail(FIV_IS_SIDEBAR(self), NULL);
|
||||
return GTK_BOX(self->toolbar);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fiv-io.h"
|
||||
#include "fiv-io-model.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
@@ -26,4 +26,3 @@ G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow)
|
||||
|
||||
GtkWidget *fiv_sidebar_new(FivIoModel *model);
|
||||
void fiv_sidebar_show_enter_location(FivSidebar *self);
|
||||
GtkBox *fiv_sidebar_get_toolbar(FivSidebar *self);
|
||||
|
||||
862
fiv-thumbnail.c
862
fiv-thumbnail.c
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// fiv-thumbnail.h: thumbnail management
|
||||
//
|
||||
// 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.
|
||||
@@ -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") \
|
||||
@@ -45,24 +45,31 @@ typedef struct _FivThumbnailSizeInfo {
|
||||
const char *thumbnail_spec_name; ///< thumbnail-spec directory name
|
||||
} FivThumbnailSizeInfo;
|
||||
|
||||
extern FivThumbnailSizeInfo fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_COUNT];
|
||||
|
||||
enum { FIV_THUMBNAIL_WIDE_COEFFICIENT = 2 };
|
||||
|
||||
extern FivThumbnailSizeInfo fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_COUNT];
|
||||
|
||||
/// If non-NULL, indicates a thumbnail of insufficient quality.
|
||||
extern cairo_user_data_key_t fiv_thumbnail_key_lq;
|
||||
|
||||
/// Returns this user's root thumbnail directory.
|
||||
gchar *fiv_thumbnail_get_root(void);
|
||||
/// Attempts to extract any low-quality thumbnail from fast targets.
|
||||
/// If `max_size` is a valid value, the image will be downscaled as appropriate.
|
||||
cairo_surface_t *fiv_thumbnail_extract(
|
||||
GFile *target, FivThumbnailSize max_size, GError **error);
|
||||
|
||||
/// Generates wide thumbnails of up to the specified size, saves them in cache.
|
||||
/// Returns the surface used for the maximum size (if the pointer was NULL).
|
||||
gboolean fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
|
||||
cairo_surface_t **max_size_surface, GError **error);
|
||||
/// Returns the surface used for the maximum size, or an error.
|
||||
cairo_surface_t *fiv_thumbnail_produce(
|
||||
GFile *target, FivThumbnailSize max_size, GError **error);
|
||||
|
||||
/// Like fiv_thumbnail_produce(), but skips the cache.
|
||||
cairo_surface_t *fiv_thumbnail_produce_for_search(
|
||||
GFile *target, FivThumbnailSize max_size, GError **error);
|
||||
|
||||
/// Retrieves a thumbnail of the most appropriate quality and resolution
|
||||
/// for the target file.
|
||||
cairo_surface_t *fiv_thumbnail_lookup(GFile *target, 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);
|
||||
|
||||
8
fiv-update-desktop-files.in
Executable file
8
fiv-update-desktop-files.in
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh -e
|
||||
fiv=${DESTDIR:+$DESTDIR/}'@FIV@'
|
||||
desktopdir=${DESTDIR:+$DESTDIR/}'@DESKTOPDIR@'
|
||||
|
||||
types=$("$fiv" --list-supported-media-types | tr '\n' ';')
|
||||
for desktop in @DESKTOPS@
|
||||
do sed -i "s|^MimeType=.*|MimeType=$types|" "$desktopdir"/"$desktop"
|
||||
done
|
||||
1464
fiv-view.c
1464
fiv-view.c
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ G_DECLARE_FINAL_TYPE(FivView, fiv_view, FIV, VIEW, GtkWidget)
|
||||
|
||||
/// Try to open the given file, synchronously, to be displayed by the widget.
|
||||
/// The current image is cleared on failure.
|
||||
gboolean fiv_view_set_uri(FivView *self, const gchar *uri);
|
||||
gboolean fiv_view_set_uri(FivView *self, const char *uri);
|
||||
|
||||
// And this is how you avoid glib-mkenums.
|
||||
typedef enum _FivViewCommand {
|
||||
@@ -50,6 +50,7 @@ typedef enum _FivViewCommand {
|
||||
XX(FIV_VIEW_COMMAND_TOGGLE_FILTER, "toggle-filter") \
|
||||
XX(FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD, "toggle-checkerboard") \
|
||||
XX(FIV_VIEW_COMMAND_TOGGLE_ENHANCE, "toggle-enhance") \
|
||||
XX(FIV_VIEW_COMMAND_COPY, "copy") \
|
||||
XX(FIV_VIEW_COMMAND_PRINT, "print") \
|
||||
XX(FIV_VIEW_COMMAND_SAVE_PAGE, "save-page") \
|
||||
XX(FIV_VIEW_COMMAND_SAVE_FRAME, "save-frame") \
|
||||
@@ -60,7 +61,8 @@ typedef enum _FivViewCommand {
|
||||
XX(FIV_VIEW_COMMAND_ZOOM_1, "zoom-1") \
|
||||
XX(FIV_VIEW_COMMAND_FIT_WIDTH, "fit-width") \
|
||||
XX(FIV_VIEW_COMMAND_FIT_HEIGHT, "fit-height") \
|
||||
XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit")
|
||||
XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit") \
|
||||
XX(FIV_VIEW_COMMAND_TOGGLE_FIXATE, "toggle-fixate")
|
||||
#define XX(constant, name) constant,
|
||||
FIV_VIEW_COMMANDS(XX)
|
||||
#undef XX
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
Type=Application
|
||||
Name=fiv
|
||||
GenericName=Image Viewer
|
||||
X-GNOME-FullName=fiv Image Viewer
|
||||
Icon=fiv
|
||||
Exec=fiv -- %u
|
||||
Exec=fiv -- %U
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
Categories=Graphics;2DGraphics;Viewer;
|
||||
|
||||
48
fiv.gschema.xml
Normal file
48
fiv.gschema.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<schemalist>
|
||||
<enum id="name.janouch.fiv.thumbnail-size">
|
||||
<value nick='Small' value='0'/>
|
||||
<value nick='Normal' value='1'/>
|
||||
<value nick='Large' value='2'/>
|
||||
<value nick='Huge' value='3'/>
|
||||
</enum>
|
||||
|
||||
<schema path="/name/janouch/fiv/" id="name.janouch.fiv">
|
||||
<key name='native-view-window' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Use a native window for the view</summary>
|
||||
<description>
|
||||
On X11, using native GdkWindows enables use of 30-bit Visuals
|
||||
(that is, 10 bits per channel), at the cost of disabling
|
||||
double buffering.
|
||||
</description>
|
||||
</key>
|
||||
<key name='opengl' type='b'>
|
||||
<default>false</default>
|
||||
<summary>Use experimental OpenGL rendering</summary>
|
||||
<description>
|
||||
OpenGL within GTK+ is highly problematic--you don't want this.
|
||||
</description>
|
||||
</key>
|
||||
<key name='dark-theme' type='b'>
|
||||
<default>false</default>
|
||||
<summary>Use a dark theme variant on start-up</summary>
|
||||
</key>
|
||||
<key name='show-browser-sidebar' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Show the browser's sidebar</summary>
|
||||
</key>
|
||||
<key name='show-browser-toolbar' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Show a toolbar in the browser view</summary>
|
||||
</key>
|
||||
<key name='show-view-toolbar' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Show a toolbar in the image view</summary>
|
||||
</key>
|
||||
<key name='thumbnail-size' enum='name.janouch.fiv.thumbnail-size'>
|
||||
<default>'Normal'</default>
|
||||
<summary>Thumbnail size to assume on start-up</summary>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
11
fiv.manifest
Normal file
11
fiv.manifest
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity name="fiv" version="1.0.0.0" type="win32" />
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0" type="win32" processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df" language="*" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
3
fiv.rc
Normal file
3
fiv.rc
Normal file
@@ -0,0 +1,3 @@
|
||||
#include <windows.h>
|
||||
LD_ICON ICON "fiv.ico"
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "fiv.manifest"
|
||||
71
fiv.wxs.in
Normal file
71
fiv.wxs.in
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||
<?define FullName = "@ProjectName@ @ProjectVersion@" ?>
|
||||
<?if $(sys.BUILDARCH) = x64 ?>
|
||||
<?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else?>
|
||||
<?define ProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?endif?>
|
||||
|
||||
<Product Id='*'
|
||||
Name='$(var.FullName)'
|
||||
UpgradeCode='a3e64e2d-4310-4c5f-8562-bb0e0b3e0a53'
|
||||
Language='1033'
|
||||
Codepage='1252'
|
||||
Version='@ProjectVersion@'
|
||||
Manufacturer='Premysl Eric Janouch'>
|
||||
|
||||
<Package Id='*'
|
||||
Keywords='Installer,Image,Viewer'
|
||||
Description='$(var.FullName) Installer'
|
||||
Manufacturer='Premysl Eric Janouch'
|
||||
InstallerVersion='200'
|
||||
Compressed='yes'
|
||||
Languages='1033'
|
||||
SummaryCodepage='1252' />
|
||||
|
||||
<Media Id='1' Cabinet='data.cab' EmbedCab='yes' />
|
||||
<Icon Id='fiv.ico' SourceFile='fiv.ico' />
|
||||
<Property Id='ARPPRODUCTICON' Value='fiv.ico' />
|
||||
<Property Id='ARPURLINFOABOUT' Value='@ProjectURL@' />
|
||||
|
||||
<UIRef Id='WixUI_Minimal' />
|
||||
<!-- This isn't supported by msitools, but is necessary for WiX.
|
||||
<WixVariable Id='WixUILicenseRtf' Value='License.rtf' />
|
||||
-->
|
||||
|
||||
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||
<Directory Id='$(var.ProgramFilesFolder)'>
|
||||
<Directory Id='INSTALLDIR' Name='$(var.FullName)' />
|
||||
</Directory>
|
||||
|
||||
<Directory Id='ProgramMenuFolder'>
|
||||
<Directory Id='ProgramMenuDir' Name='$(var.FullName)' />
|
||||
</Directory>
|
||||
|
||||
<Directory Id='DesktopFolder' />
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id='ProgramMenuDir'>
|
||||
<Component Id='ProgramMenuDir' Guid='*'>
|
||||
<Shortcut Id='ProgramsMenuShortcut'
|
||||
Name='@ProjectName@'
|
||||
Target='[INSTALLDIR]\fiv.exe'
|
||||
WorkingDirectory='INSTALLDIR'
|
||||
Arguments='"%USERPROFILE%"'
|
||||
Icon='fiv.ico' />
|
||||
<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
|
||||
<RegistryValue Root='HKCU'
|
||||
Key='Software\[Manufacturer]\[ProductName]'
|
||||
Type='string'
|
||||
Value=''
|
||||
KeyPath='yes' />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentGroupRef Id='CG.fiv' />
|
||||
<ComponentRef Id='ProgramMenuDir' />
|
||||
</Feature>
|
||||
</Product>
|
||||
</Wix>
|
||||
329
meson.build
329
meson.build
@@ -1,7 +1,8 @@
|
||||
# vim: noet ts=4 sts=4 sw=4:
|
||||
project('fiv', 'c',
|
||||
default_options : ['c_std=gnu99', 'warning_level=2'],
|
||||
version : '0.1.0')
|
||||
version : '1.0.0',
|
||||
meson_version : '>=0.57')
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
add_project_arguments(
|
||||
@@ -16,6 +17,8 @@ add_project_arguments(
|
||||
# add_project_link_arguments(flags, language : ['c'])
|
||||
#endif
|
||||
|
||||
win32 = host_machine.system() == 'windows'
|
||||
|
||||
# The likelihood of this being installed is nearly zero. Enable the wrap.
|
||||
libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
|
||||
allow_fallback : true)
|
||||
@@ -23,27 +26,25 @@ 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'),
|
||||
|
||||
# Wuffs is included as a submodule.
|
||||
dependency('libjpeg'),
|
||||
dependency('libturbojpeg'),
|
||||
dependency('libwebp'),
|
||||
dependency('libwebpdemux'),
|
||||
dependency('libwebpdecoder', required : false),
|
||||
dependency('libwebpmux'),
|
||||
# https://github.com/google/wuffs/issues/58
|
||||
dependency('spng', version : '>=0.7.0',
|
||||
default_options: 'default_library=static',
|
||||
# fallback : ['spng', 'spng_dep'],
|
||||
),
|
||||
# Wuffs is included as a submodule.
|
||||
|
||||
lcms2,
|
||||
libjpegqs,
|
||||
@@ -56,13 +57,31 @@ 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
|
||||
if not get_option('resvg').disabled()
|
||||
resvg = dependency('resvg', required : false)
|
||||
if not resvg.found()
|
||||
resvg = cc.find_library('libresvg', required : get_option('resvg'))
|
||||
resvg = cc.find_library('resvg', required : get_option('resvg'))
|
||||
if resvg.found() and not cc.has_header('resvg.h')
|
||||
error('resvg.h not found')
|
||||
endif
|
||||
@@ -75,13 +94,22 @@ endif
|
||||
|
||||
# XXX: https://github.com/mesonbuild/meson/issues/825
|
||||
docdir = get_option('datadir') / 'doc' / meson.project_name()
|
||||
application_ns = 'name.janouch.'
|
||||
application_url = 'https://janouch.name/p/' + meson.project_name()
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set_quoted('PROJECT_NAME', meson.project_name())
|
||||
conf.set_quoted('PROJECT_VERSION', meson.project_version())
|
||||
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
|
||||
conf.set_quoted('PROJECT_NS', application_ns)
|
||||
conf.set_quoted('PROJECT_URL', application_url)
|
||||
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
|
||||
if win32
|
||||
conf.set_quoted('PROJECT_DOCDIR', docdir)
|
||||
endif
|
||||
|
||||
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())
|
||||
@@ -89,57 +117,268 @@ conf.set('HAVE_XCURSOR', xcursor.found())
|
||||
conf.set('HAVE_LIBHEIF', libheif.found())
|
||||
conf.set('HAVE_LIBTIFF', libtiff.found())
|
||||
conf.set('HAVE_GDKPIXBUF', gdkpixbuf.found())
|
||||
configure_file(
|
||||
|
||||
config = vcs_tag(
|
||||
command : ['git', 'describe', '--always', '--dirty=+'],
|
||||
input : configure_file(output : 'config.h.in', configuration : conf),
|
||||
output : 'config.h',
|
||||
configuration : conf,
|
||||
)
|
||||
|
||||
rc = []
|
||||
if win32
|
||||
windows = import('windows')
|
||||
rsvg_convert = find_program('rsvg-convert')
|
||||
icotool = find_program('icotool')
|
||||
|
||||
# Meson is brain-dead and retarded, so these PNGs cannot be installed,
|
||||
# only because they must all have the same name when installed.
|
||||
# The largest size is mainly for an appropriately sized Windows icon.
|
||||
icon_png_list = []
|
||||
foreach size : ['16', '32', '48', '256']
|
||||
icon_dimensions = size + 'x' + size
|
||||
icon_png_list += custom_target(icon_dimensions + ' icon',
|
||||
input : 'fiv.svg',
|
||||
output : 'fiv.' + icon_dimensions + '.png',
|
||||
command : [rsvg_convert, '--output', '@OUTPUT@',
|
||||
'--width', size, '--height', size, '@INPUT@'])
|
||||
endforeach
|
||||
|
||||
icon_ico = custom_target('fiv.ico',
|
||||
output : 'fiv.ico', input : icon_png_list,
|
||||
command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
|
||||
rc += windows.compile_resources('fiv.rc', depends : icon_ico)
|
||||
endif
|
||||
|
||||
gnome = import('gnome')
|
||||
resources = gnome.compile_resources('resources',
|
||||
gresources = gnome.compile_resources('resources',
|
||||
'resources/resources.gresource.xml',
|
||||
source_dir : 'resources',
|
||||
c_name : 'resources',
|
||||
)
|
||||
|
||||
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-io.c',
|
||||
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'xdg.c', resources,
|
||||
install : true,
|
||||
dependencies : [dependencies])
|
||||
if gdkpixbuf.found()
|
||||
executable('io-benchmark', 'fiv-io-benchmark.c', 'fiv-io.c', 'xdg.c',
|
||||
build_by_default : false,
|
||||
dependencies : [dependencies, gdkpixbuf])
|
||||
endif
|
||||
tiff_tables = custom_target('tiff-tables.h',
|
||||
output : 'tiff-tables.h',
|
||||
input : 'tiff-tables.db',
|
||||
# Meson 0.56 chokes on files() as well as on a relative path.
|
||||
command : [meson.current_source_dir() / 'tiff-tables.awk', '@INPUT@'],
|
||||
capture : true,
|
||||
)
|
||||
|
||||
jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c',
|
||||
desktops = ['fiv.desktop', 'fiv-browse.desktop']
|
||||
iolib = static_library('fiv-io', 'fiv-io.c', 'fiv-io-cmm.c', 'xdg.c',
|
||||
tiff_tables, config,
|
||||
dependencies : dependencies).extract_all_objects(recursive : true)
|
||||
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
|
||||
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
|
||||
'fiv-io-model.c', gresources, rc, config,
|
||||
objects : iolib,
|
||||
dependencies : dependencies,
|
||||
install : true,
|
||||
win_subsystem : 'windows',
|
||||
)
|
||||
|
||||
desktops += 'fiv-jpegcrop.desktop'
|
||||
jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
|
||||
install : true,
|
||||
dependencies : [
|
||||
dependency('gtk+-3.0'),
|
||||
dependency('libturbojpeg'),
|
||||
])
|
||||
install_data('fiv-jpegcrop.desktop',
|
||||
install_dir : get_option('datadir') / 'applications')
|
||||
],
|
||||
win_subsystem : 'windows',
|
||||
)
|
||||
|
||||
# XXX: With gdk-pixbuf, this even depends on currently installed modules.
|
||||
if meson.is_cross_build()
|
||||
install_data('fiv.desktop',
|
||||
install_dir : get_option('datadir') / 'applications')
|
||||
else
|
||||
# XXX: The exe path may not contain spaces--quoting is a bitch in Meson.
|
||||
desktop = custom_target('desktop',
|
||||
output : 'fiv.desktop',
|
||||
input : 'fiv.desktop',
|
||||
command : ['sh', '-c', '''awk '/^MimeType=/ { $0 = "MimeType=";
|
||||
while (((exe " --list-supported-media-types") | getline type) > 0)
|
||||
$0 = $0 type ";" } 1' "exe=$1" "$2"''', 'sh', exe, '@INPUT@'],
|
||||
capture : true,
|
||||
install : true,
|
||||
install_dir : get_option('datadir') / 'applications',
|
||||
)
|
||||
if get_option('tools').enabled()
|
||||
# libjq has only received a pkg-config file in version 1.7.
|
||||
# libjq >= 1.6 is required.
|
||||
tools_dependencies = [
|
||||
cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
|
||||
tools_c_args = cc.get_supported_arguments(
|
||||
'-Wno-unused-function', '-Wno-unused-parameter')
|
||||
foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
|
||||
executable(tool, 'tools/' + tool + '.c', tiff_tables,
|
||||
dependencies : tools_dependencies,
|
||||
c_args: tools_c_args)
|
||||
endforeach
|
||||
|
||||
if gdkpixbuf.found()
|
||||
executable('benchmark-io', 'tools/benchmark-io.c',
|
||||
objects : iolib,
|
||||
dependencies : [dependencies, gdkpixbuf])
|
||||
endif
|
||||
endif
|
||||
|
||||
install_data('fiv-browse.desktop',
|
||||
install_dir : get_option('datadir') / 'applications')
|
||||
# Copying the files to the build directory makes GSettings find them in devenv.
|
||||
gsettings_schemas = ['fiv.gschema.xml']
|
||||
foreach schema : gsettings_schemas
|
||||
configure_file(
|
||||
input : schema,
|
||||
output : application_ns + schema,
|
||||
copy : true,
|
||||
install: true,
|
||||
install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
|
||||
endforeach
|
||||
|
||||
# For the purposes of development: make the program find its GSettings schemas.
|
||||
gnome.compile_schemas(depend_files : files(gsettings_schemas))
|
||||
gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
|
||||
|
||||
# Meson is broken on Windows and removes the backslashes, so this ends up empty.
|
||||
symbolics = run_command(find_program('sed', required : false, disabler : true),
|
||||
'-n', 's@.*>\\([^<>]*[.]svg\\)<.*@resources/\\1@p',
|
||||
configure_file(
|
||||
input : 'resources/resources.gresource.xml',
|
||||
output : 'resources.gresource.xml.stamp',
|
||||
copy : true,
|
||||
), capture : true, check : true).stdout().strip()
|
||||
|
||||
# Validate various files, if there are tools around to do it.
|
||||
xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
|
||||
gsettings_schemas
|
||||
if symbolics != ''
|
||||
xmls += symbolics.split('\n')
|
||||
endif
|
||||
|
||||
xmlwf = find_program('xmlwf', required : false, disabler : true)
|
||||
xmllint = find_program('xmllint', required : false, disabler : true)
|
||||
foreach xml : xmls
|
||||
test('xmlwf ' + xml, xmlwf, args : files(xml))
|
||||
test('xmllint ' + xml, xmllint, args : ['--noout', files(xml)])
|
||||
endforeach
|
||||
|
||||
dfv = find_program('desktop-file-validate', required : false, disabler : true)
|
||||
foreach desktop : desktops
|
||||
test(desktop, dfv, args : files(desktop))
|
||||
endforeach
|
||||
|
||||
# Finish the installation.
|
||||
install_data('fiv.svg',
|
||||
install_dir : get_option('datadir') / 'icons/hicolor/scalable/apps')
|
||||
install_subdir('docs', install_dir : docdir, strip_directory : true)
|
||||
install_subdir('docs',
|
||||
install_dir : docdir, strip_directory : true)
|
||||
|
||||
if not win32
|
||||
asciidoctor = find_program('asciidoctor', required : false)
|
||||
a2x = find_program('a2x', required : false)
|
||||
if not asciidoctor.found() and not a2x.found()
|
||||
warning('Neither asciidoctor nor a2x were found, ' +
|
||||
'falling back to a substandard manual page generator')
|
||||
endif
|
||||
foreach page : [meson.project_name()]
|
||||
man_capture = false
|
||||
if asciidoctor.found()
|
||||
command = [asciidoctor, '-b', 'manpage',
|
||||
'-a', 'release-version=' + meson.project_version(),
|
||||
'-o', '@OUTPUT@', '@INPUT@']
|
||||
elif a2x.found()
|
||||
command = [a2x, '--doctype', 'manpage', '--format', 'manpage',
|
||||
'-a', 'release-version=' + meson.project_version(),
|
||||
'-D', '@OUTDIR@', '@INPUT@']
|
||||
else
|
||||
command = ['env', 'LC_ALL=C',
|
||||
'asciidoc-release-version=' + meson.project_version(),
|
||||
'awk', '-f', files('submodules/liberty/tools/asciiman.awk'),
|
||||
'@INPUT@']
|
||||
man_capture = true
|
||||
endif
|
||||
custom_target('manpage for ' + page,
|
||||
input : 'docs' / page + '.adoc',
|
||||
output : page + '.1',
|
||||
capture : man_capture,
|
||||
command : command,
|
||||
install : true,
|
||||
install_dir : get_option('mandir') / 'man1')
|
||||
endforeach
|
||||
|
||||
foreach desktop : desktops
|
||||
install_data(desktop,
|
||||
rename : application_ns + desktop,
|
||||
install_dir : get_option('datadir') / 'applications')
|
||||
endforeach
|
||||
|
||||
# TODO(p): Consider moving this to /usr/share or /usr/lib.
|
||||
install_data('fiv-reverse-search',
|
||||
install_dir : get_option('bindir'))
|
||||
|
||||
# As usual, handling generated files in Meson is a fucking pain.
|
||||
updatable_desktops = [application_ns + 'fiv.desktop']
|
||||
foreach name, uri : {
|
||||
'Google' : 'https://lens.google.com/uploadbyurl?url=',
|
||||
'Bing' : 'https://www.bing.com/images/searchbyimage?cbir=sbi&imgurl=',
|
||||
'Yandex' : 'https://yandex.com/images/search?rpt=imageview&url=',
|
||||
'TinEye' : 'https://tineye.com/search?url=',
|
||||
'SauceNAO' : 'https://saucenao.com/search.php?url=',
|
||||
'IQDB' : 'https://iqdb.org/?url=',
|
||||
}
|
||||
desktop = 'fiv-reverse-search-' + name.to_lower() + '.desktop'
|
||||
updatable_desktops += application_ns + desktop
|
||||
|
||||
test(desktop, dfv, args : configure_file(
|
||||
input : 'fiv-reverse-search.desktop.in',
|
||||
output : application_ns + desktop,
|
||||
configuration : {'NAME' : name, 'URL' : uri},
|
||||
install : true,
|
||||
install_dir : get_option('datadir') / 'applications',
|
||||
))
|
||||
endforeach
|
||||
|
||||
# With gdk-pixbuf, fiv.desktop depends on currently installed modules;
|
||||
# the package manager needs to be told to run this script as necessary.
|
||||
dynamic_desktops = gdkpixbuf.found()
|
||||
|
||||
updater = configure_file(
|
||||
input : 'fiv-update-desktop-files.in',
|
||||
output : 'fiv-update-desktop-files',
|
||||
configuration : {
|
||||
'FIV' : get_option('prefix') / get_option('bindir') / exe.name(),
|
||||
'DESKTOPDIR' : get_option('prefix') /
|
||||
get_option('datadir') / 'applications',
|
||||
'DESKTOPS' : ' \\\n\t'.join(updatable_desktops),
|
||||
},
|
||||
install : dynamic_desktops,
|
||||
install_dir : get_option('bindir'))
|
||||
if not meson.is_cross_build()
|
||||
meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
|
||||
endif
|
||||
|
||||
# Quick and dirty package generation, lacking dependencies.
|
||||
packaging = configuration_data({
|
||||
'name' : meson.project_name(),
|
||||
'version' : meson.project_version(),
|
||||
'summary' : 'Image viewer',
|
||||
'author' : 'Přemysl Eric Janouch',
|
||||
})
|
||||
|
||||
subdir('submodules/liberty/meson/packaging')
|
||||
elif meson.is_cross_build()
|
||||
# Note that even compiling /from within MSYS2/ can still be a cross-build.
|
||||
msys2_root = meson.get_external_property('msys2_root')
|
||||
meson.add_install_script('msys2-install.sh', msys2_root)
|
||||
|
||||
wxs = configure_file(
|
||||
input : 'fiv.wxs.in',
|
||||
output : 'fiv.wxs',
|
||||
configuration : configuration_data({
|
||||
'ProjectName' : meson.project_name(),
|
||||
'ProjectVersion' : meson.project_version(),
|
||||
'ProjectURL' : application_url,
|
||||
}),
|
||||
)
|
||||
msi = meson.project_name() + '-' + meson.project_version() + \
|
||||
'-' + host_machine.cpu() + '.msi'
|
||||
custom_target('package',
|
||||
output : msi,
|
||||
command : [meson.current_source_dir() / 'msys2-package.sh',
|
||||
host_machine.cpu(), msi, wxs],
|
||||
env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
|
||||
'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
|
||||
console : true,
|
||||
build_always_stale : true,
|
||||
build_by_default : false,
|
||||
)
|
||||
|
||||
# This is the minimum to run targets from msys2-configure.sh builds.
|
||||
meson.add_devenv({
|
||||
'WINEPATH' : msys2_root / 'bin',
|
||||
'XDG_DATA_DIRS' : msys2_root / 'share',
|
||||
})
|
||||
endif
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
option('tools', type : 'feature', value : 'disabled',
|
||||
description : 'Build a few extra file inspection tools')
|
||||
|
||||
option('lcms2', type : 'feature', value : 'auto',
|
||||
description : 'Build with Little CMS colour management')
|
||||
option('lcms2fastfloat', type : 'feature', value : 'auto',
|
||||
description : 'Build with Little CMS fast float plugin support')
|
||||
option('libjpegqs', type : 'feature', value : 'auto',
|
||||
description : 'Build with JPEG Quant Smooth integration')
|
||||
option('libraw', type : 'feature', value : 'auto',
|
||||
|
||||
149
msys2-configure.sh
Executable file
149
msys2-configure.sh
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/bin/sh -e
|
||||
# msys2-configure.sh: set up an MSYS2-based Meson build (x86-64 by default)
|
||||
#
|
||||
# Dependencies: AWK, sed, coreutils, cURL, bsdtar (libarchive),
|
||||
# wine64, Meson, mingw-w64-binutils, mingw-w64-gcc, pkg-config
|
||||
#
|
||||
# We support running directly from within MSYS2 on Windows,
|
||||
# albeit while still downloading a complete copy of runtime depencies.
|
||||
pkg=${MINGW_PACKAGE_PREFIX:-mingw-w64-x86_64}
|
||||
prefix=${MSYSTEM_PREFIX:-/mingw64}
|
||||
repo=https://repo.msys2.org/mingw$prefix
|
||||
|
||||
chost=${MSYSTEM_CHOST:-x86_64-w64-mingw32}
|
||||
carch=${MSYSTEM_CARCH:-x86_64}
|
||||
[ "$carch" = "i686" ] && carch=x86
|
||||
|
||||
if [ -n "$MSYSTEM" ]
|
||||
then
|
||||
wine64() { "$@"; }
|
||||
awk() { command awk -v RS='\r?\n' "$@"; }
|
||||
pacman -S --needed libarchive $pkg-ca-certificates $pkg-gcc $pkg-icoutils \
|
||||
$pkg-librsvg $pkg-meson $pkg-msitools $pkg-pkgconf
|
||||
fi
|
||||
|
||||
status() {
|
||||
echo "$(tput bold)-- $*$(tput sgr0)"
|
||||
}
|
||||
|
||||
dbsync() {
|
||||
status Fetching repository DB
|
||||
[ -f db.tsv ] || curl -# "$repo$prefix.db" | bsdtar -xOf- | awk '
|
||||
function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] }
|
||||
NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
|
||||
!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
|
||||
field == "%SHA256SUM%" { path = "*packages/" f["%FILENAME%"]
|
||||
sub(/\t$/, "", path); print $0, path > "db.sums" } END { flush() }
|
||||
' > db.tsv
|
||||
}
|
||||
|
||||
fetch() {
|
||||
status Resolving "$@"
|
||||
mkdir -p packages
|
||||
awk -F'\t' 'function get(name, i, a) {
|
||||
if (visited[name]++ || !(name in filenames)) return
|
||||
print filenames[name]; split(deps[name], a); for (i in a) get(a[i])
|
||||
} BEGIN { while ((getline < "db.tsv") > 0) {
|
||||
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
|
||||
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
|
||||
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
|
||||
while IFS= read -r name
|
||||
do
|
||||
status Fetching "$name"
|
||||
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
|
||||
done
|
||||
|
||||
version=$(curl -# https://exiftool.org/ver.txt)
|
||||
name=exiftool-$version.tar.gz remotename=Image-ExifTool-$version.tar.gz
|
||||
status Fetching "$remotename"
|
||||
[ -f "$name" ] || curl -#o "$name" "https://exiftool.org/$remotename"
|
||||
ln -sf "$name" exiftool.tar.gz
|
||||
}
|
||||
|
||||
verify() {
|
||||
status Verifying checksums
|
||||
sha256sum --ignore-missing --quiet -c db.sums
|
||||
}
|
||||
|
||||
extract() {
|
||||
status Extracting packages
|
||||
for subdir in *
|
||||
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
|
||||
done
|
||||
while IFS= read -r name
|
||||
do bsdtar -xf "packages/$name" --strip-components 1 \
|
||||
--exclude '*/share/man' --exclude '*/share/doc'
|
||||
done < db.want
|
||||
|
||||
# Don't require Perl, which may not exist anymore on i686:
|
||||
# https://github.com/msys2/MINGW-packages/pull/20085
|
||||
if [ -d lib/perl5 ]
|
||||
then
|
||||
bsdtar -xf exiftool.tar.gz
|
||||
mv Image-ExifTool-*/exiftool bin
|
||||
mv Image-ExifTool-*/lib/* lib/perl5/site_perl
|
||||
rm -rf Image-ExifTool-*
|
||||
fi
|
||||
}
|
||||
|
||||
configure() {
|
||||
# Don't let GLib programs inherit wrong paths from the environment.
|
||||
export XDG_DATA_DIRS=$msys2_root/share
|
||||
|
||||
status Configuring packages
|
||||
wine64 bin/update-mime-database.exe share/mime
|
||||
wine64 bin/glib-compile-schemas.exe share/glib-2.0/schemas
|
||||
wine64 bin/gdk-pixbuf-query-loaders.exe \
|
||||
> lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
|
||||
}
|
||||
|
||||
setup() {
|
||||
status Setting up Meson
|
||||
wrap=true pclibdir=$msys2_root/share/pkgconfig:$msys2_root/lib/pkgconfig
|
||||
[ -n "$MSYSTEM" ] && \
|
||||
wrap=false pclibdir="$(pwd -W)/share/pkgconfig;$(pwd -W)/lib/pkgconfig"
|
||||
|
||||
cat >"$toolchain" <<-EOF
|
||||
[binaries]
|
||||
c = '$chost-gcc'
|
||||
cpp = '$chost-g++'
|
||||
ar = '$chost-gcc-ar'
|
||||
ranlib = '$chost-gcc-ranlib'
|
||||
strip = '$chost-strip'
|
||||
windres = '$chost-windres'
|
||||
pkgconfig = 'pkg-config'
|
||||
|
||||
[properties]
|
||||
sys_root = '$builddir'
|
||||
msys2_root = '$msys2_root'
|
||||
pkg_config_libdir = '$pclibdir'
|
||||
needs_exe_wrapper = $wrap
|
||||
|
||||
[host_machine]
|
||||
system = 'windows'
|
||||
cpu_family = '$carch'
|
||||
cpu = '$carch'
|
||||
endian = 'little'
|
||||
EOF
|
||||
|
||||
meson setup --buildtype=debugoptimized --prefix=/ \
|
||||
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
|
||||
}
|
||||
|
||||
sourcedir=$(realpath "${2:-$(dirname "$0")}")
|
||||
builddir=$(realpath "${1:-builddir}")
|
||||
toolchain=$builddir/msys2-cross-toolchain.meson
|
||||
|
||||
# This directory name matches the prefix in .pc files, so we don't need to
|
||||
# modify them (pkgconf has --prefix-variable, but Meson can't pass that option).
|
||||
msys2_root=$builddir$prefix
|
||||
|
||||
mkdir -p "$msys2_root"
|
||||
cd "$msys2_root"
|
||||
dbsync
|
||||
fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-libjxl $pkg-perl \
|
||||
$pkg-perl-win32-api $pkg-libwinpthread-git # Because we don't do "provides"?
|
||||
verify
|
||||
extract
|
||||
configure
|
||||
setup
|
||||
77
msys2-install.sh
Executable file
77
msys2-install.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh -e
|
||||
export LC_ALL=C
|
||||
cd "$MESON_INSTALL_DESTDIR_PREFIX"
|
||||
msys2_root=$1
|
||||
|
||||
# Support running directly from within MSYS2 on Windows.
|
||||
if [ -n "$MSYSTEM" ]
|
||||
then
|
||||
wine64() { "$@"; }
|
||||
awk() { command awk -v RS='\r?\n' "$@"; }
|
||||
fi
|
||||
|
||||
# Copy binaries we directly or indirectly depend on.
|
||||
cp -p "$msys2_root"/bin/*.dll .
|
||||
cp -p "$msys2_root"/bin/wperl.exe . || :
|
||||
cp -p "$msys2_root"/bin/exiftool . || :
|
||||
# The console helper is only useful for debug builds.
|
||||
cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
|
||||
cp -pR "$msys2_root"/etc/ .
|
||||
|
||||
mkdir -p lib
|
||||
cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib
|
||||
cp -pR "$msys2_root"/lib/perl5/ lib || :
|
||||
mkdir -p share/glib-2.0/schemas
|
||||
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
|
||||
mkdir -p share/icons
|
||||
cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons
|
||||
mkdir -p share/icons/hicolor
|
||||
cp -p "$msys2_root"/share/icons/hicolor/index.theme share/icons/hicolor
|
||||
mkdir -p share/mime
|
||||
# GIO doesn't use the database on Windows, this subset is for us.
|
||||
find "$msys2_root"/share/mime/ -maxdepth 1 -type f -exec cp -p {} share/mime \;
|
||||
|
||||
# Remove unreferenced libraries.
|
||||
find lib -name '*.a' -exec rm -- {} +
|
||||
awk 'function whitelist(binary) {
|
||||
if (seen[binary]++)
|
||||
return
|
||||
|
||||
delete orphans[binary]
|
||||
while (("strings -a \"" binary "\" 2>/dev/null" | getline string) > 0)
|
||||
if (match(string, /[-.+_a-zA-Z0-9]+[.][Dd][Ll][Ll]$/))
|
||||
whitelist("./" substr(string, RSTART, RLENGTH))
|
||||
} BEGIN {
|
||||
while (("find . -type f -path \"./*.[Dd][Ll][Ll]\"" | getline) > 0)
|
||||
orphans[$0]++
|
||||
while (("find . -type f -path \"./*.[Ee][Xx][Ee]\"" | getline) > 0)
|
||||
whitelist($0)
|
||||
while (("find ./lib -type f -path \"./*.[Dd][Ll][Ll]\"" | getline) > 0)
|
||||
whitelist($0)
|
||||
for (library in orphans)
|
||||
print library
|
||||
}' | xargs rm --
|
||||
|
||||
# Removes unused icons from the Adwaita theme. It could be even more aggressive,
|
||||
# since it keeps around lots of sizes and all the GTK+ stock icons.
|
||||
find share/icons/Adwaita -type f | awk 'BEGIN {
|
||||
while (("grep -aho \"[a-z][a-z-]*\" *.dll *.exe" | getline) > 0)
|
||||
good[$0] = 1
|
||||
} /[.](png|svg|cur|ani)$/ {
|
||||
# Cut out the basename without extensions.
|
||||
match($0, /[^\/]+$/)
|
||||
base = substr($0, RSTART)
|
||||
sub(/[.].+$/, "", base)
|
||||
|
||||
# Try matching while cutting off suffixes.
|
||||
# Disregarding the not-much-used GTK_ICON_LOOKUP_GENERIC_FALLBACK.
|
||||
while (!(keep = good[base]) &&
|
||||
sub(/-(ltr|rtl|symbolic)$/, "", base)) {}
|
||||
if (!keep)
|
||||
print
|
||||
}' | xargs rm --
|
||||
|
||||
wine64 "$msys2_root"/bin/glib-compile-schemas.exe share/glib-2.0/schemas
|
||||
|
||||
# This may speed up program start-up a little bit.
|
||||
wine64 "$msys2_root"/bin/gtk-update-icon-cache-3.0.exe share/icons/Adwaita
|
||||
35
msys2-package.sh
Executable file
35
msys2-package.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh -e
|
||||
export LC_ALL=C
|
||||
cd "$MESON_BUILD_ROOT"
|
||||
arch=$1 msi=$2 files=package-files.wxs
|
||||
destdir=$(pwd)/package/${msi%.*}
|
||||
shift 2
|
||||
|
||||
# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
|
||||
[ "$arch" = "x86" ] || arch=x64
|
||||
|
||||
rm -rf "$destdir"
|
||||
meson install --destdir "$destdir"
|
||||
|
||||
txt2rtf() {
|
||||
LC_ALL=C.UTF-8 iconv -f utf-8 -t ascii//translit "$@" | awk 'BEGIN {
|
||||
print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
|
||||
print "\\f0\\fs24{\\pard\\sa240"
|
||||
} {
|
||||
gsub(/\\/, "\\\\"); gsub(/[{]/, "\\{"); gsub(/[}]/, "\\}")
|
||||
if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
|
||||
else { print prefix $0; prefix = " " }
|
||||
} END {
|
||||
print "\\par}}"
|
||||
}'
|
||||
}
|
||||
|
||||
# msitools have this filename hardcoded in UI files, and it's required.
|
||||
txt2rtf "$MESON_SOURCE_ROOT/LICENSE" > License.rtf
|
||||
|
||||
find "$destdir" -type f \
|
||||
| wixl-heat --prefix "$destdir/" --directory-ref INSTALLDIR \
|
||||
--component-group CG.fiv --var var.SourceDir > "$files"
|
||||
|
||||
wixl --verbose --arch "$arch" -D SourceDir="$destdir" --ext ui \
|
||||
--output "$msi" "$@" "$files"
|
||||
4
resources/cross-large-symbolic.svg
Normal file
4
resources/cross-large-symbolic.svg
Normal 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 |
152
resources/pin2-symbolic.svg
Normal file
152
resources/pin2-symbolic.svg
Normal file
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<filter id="a" height="100%" width="100%" x="0%" y="0%">
|
||||
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<mask id="b">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="c">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="d">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="e">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="f">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="g">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="h">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="i">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="j">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="k">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="l">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="m">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="n">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="o">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="p">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="q">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="r">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="s">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="t">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="u">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="v">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="w">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="x">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="y">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="z">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="A">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<path d="m 14 28 c 0 3.3125 -2.6875 6 -6 6 s -6 -2.6875 -6 -6 s 2.6875 -6 6 -6 s 6 2.6875 6 6 z m 0 0" fill="none" stroke="#2e3436" stroke-linecap="round" stroke-width="2"/>
|
||||
<path d="m 6.992188 2 l -5.070313 4.992188 l 3.261719 2.539062 c -0.519532 2.046875 0.078125 4.214844 1.566406 5.710938 l 3.742188 -3.742188 l 4.5 4.5 h 1 v -1 l -4.5 -4.5 l 3.746093 -3.742188 c -1.5 -1.496093 -3.679687 -2.089843 -5.730469 -1.5625 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
<path d="m 1.992188 7 l 5 -5" fill="none" stroke="#2e3436" stroke-linecap="square" stroke-width="2"/>
|
||||
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -620 -624)">
|
||||
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -4,11 +4,15 @@
|
||||
<file alias="LICENSE">../LICENSE</file>
|
||||
</gresource>
|
||||
<gresource prefix="/org/gnome/design/IconLibrary/scalable/actions/">
|
||||
<file preprocess="xml-stripblanks">text-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">circle-filled-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">cross-large-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">funnel-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">blend-tool-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">checkerboard-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">heal-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">info-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">pin2-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">shapes-symbolic.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
154
resources/shapes-symbolic.svg
Normal file
154
resources/shapes-symbolic.svg
Normal file
@@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<filter id="a" height="100%" width="100%" x="0%" y="0%">
|
||||
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<mask id="b">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="c">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="d">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="e">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="f">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="g">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="h">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="i">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="j">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="k">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="l">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="m">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="n">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="o">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="p">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="q">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="r">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="s">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="t">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="u">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="v">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="w">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="x">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="y">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="z">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="A">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<g fill="#2e3436">
|
||||
<path d="m 5.191406 1.296875 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 l -2.5 2.5 c -0.390625 0.390625 -0.390625 1.023437 0 1.414063 l 2.5 2.5 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2.496094 -2.5 c 0.390625 -0.390626 0.390625 -1.023438 0 -1.414063 z m 0 0"/>
|
||||
<path d="m 9.984375 12.003906 c 0 1.65625 -1.34375 3 -3 3 c -1.660156 0 -3 -1.34375 -3 -3 s 1.339844 -3 3 -3 c 1.65625 0 3 1.34375 3 3 z m 0 0"/>
|
||||
<path d="m 11.929688 2.007812 c -0.339844 0.015626 -0.644532 0.203126 -0.8125 0.496094 l -2.320313 4 c -0.386719 0.664063 0.09375 1.5 0.863281 1.5 h 4.644532 c 0.769531 0 1.25 -0.835937 0.863281 -1.5 l -2.320313 -4 c -0.1875 -0.328125 -0.542968 -0.519531 -0.917968 -0.496094 z m 0 0"/>
|
||||
</g>
|
||||
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -620 -420)">
|
||||
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
150
resources/text-symbolic.svg
Normal file
150
resources/text-symbolic.svg
Normal file
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<filter id="a" height="100%" width="100%" x="0%" y="0%">
|
||||
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<mask id="b">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="c">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="d">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="e">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="f">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="g">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="h">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="i">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="j">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="k">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="l">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="m">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="n">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="o">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="p">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="q">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="r">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="s">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="t">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="u">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="v">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="w">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="x">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="y">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<mask id="z">
|
||||
<g filter="url(#a)">
|
||||
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="A">
|
||||
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<path d="m 6 1 l -5 14 h 3 c 1.484375 -4 0.023438 0 1.507812 -4 h 4.984376 l 1.507812 4 h 3 l -5 -14 z m 2 3 l 2.023438 5 h -4 z m 0 0" fill="#2e3436"/>
|
||||
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -56 -640)">
|
||||
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
1
submodules/liberty
Submodule
1
submodules/liberty
Submodule
Submodule submodules/liberty added at 0e86ffe7c3
1
submodules/wuffs-mirror-release-c
Submodule
1
submodules/wuffs-mirror-release-c
Submodule
Submodule submodules/wuffs-mirror-release-c added at 50869df0ea
@@ -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]
|
||||
|
||||
4
subprojects/packagefiles/libjpegqs/include/libjpegqs.h
Normal file
4
subprojects/packagefiles/libjpegqs/include/libjpegqs.h
Normal file
@@ -0,0 +1,4 @@
|
||||
// This separate directory is necessary for Debian's multiarch with jpeg-turbo,
|
||||
// because its jpeglib.h cannot perform local inclusion of jconfig.h,
|
||||
// resulting in it being found within jpeg-quantsmooth and breaking the build.
|
||||
#include "../libjpegqs.h"
|
||||
@@ -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'),
|
||||
@@ -13,29 +11,35 @@ if host_machine.cpu_family() == 'x86_64'
|
||||
jpegqs_avx512 = static_library('jpegqs-avx512', 'libjpegqs.c',
|
||||
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=avx512',
|
||||
'-mavx512f', '-mfma', '-DSIMD_AVX512'],
|
||||
dependencies : deps)
|
||||
dependencies : deps,
|
||||
implicit_include_directories : false)
|
||||
jpegqs_avx2 = static_library('jpegqs-avx2', 'libjpegqs.c',
|
||||
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=avx2',
|
||||
'-mavx2', '-mfma', '-DSIMD_AVX2'],
|
||||
dependencies : deps)
|
||||
dependencies : deps,
|
||||
implicit_include_directories : false)
|
||||
jpegqs_sse2 = static_library('jpegqs-sse2', 'libjpegqs.c',
|
||||
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=sse2', '-msse2', '-DSIMD_SSE2'],
|
||||
dependencies : deps)
|
||||
dependencies : deps,
|
||||
implicit_include_directories : false)
|
||||
jpegqs_base = static_library('jpegqs-base', 'libjpegqs.c',
|
||||
c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=base', '-DSIMD_BASE'],
|
||||
dependencies : deps)
|
||||
dependencies : deps,
|
||||
implicit_include_directories : false)
|
||||
|
||||
jpegqs_lib = static_library('jpegqs', 'libjpegqs.c',
|
||||
c_args : ['-DSIMD_SELECT'],
|
||||
dependencies : deps,
|
||||
link_with : [jpegqs_base, jpegqs_sse2, jpegqs_avx2, jpegqs_avx512])
|
||||
link_with : [jpegqs_base, jpegqs_sse2, jpegqs_avx2, jpegqs_avx512],
|
||||
implicit_include_directories : false)
|
||||
else
|
||||
jpegqs_lib = static_library('jpegqs', 'libjpegqs.c',
|
||||
c_args : ['-DNO_SIMD'],
|
||||
dependencies : deps)
|
||||
dependencies : deps,
|
||||
implicit_include_directories : false)
|
||||
endif
|
||||
|
||||
jpegqs_dep = declare_dependency(
|
||||
link_with : jpegqs_lib,
|
||||
include_directories : include_directories('.'),
|
||||
include_directories : include_directories('include'),
|
||||
)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[wrap-file]
|
||||
directory = libspng-0.7.1
|
||||
source_url = https://github.com/randy408/libspng/archive/refs/tags/v0.7.1.tar.gz
|
||||
source_filename = libspng-0.7.1.tar.gz
|
||||
source_hash = 0726a4914ad7155028f3baa94027244d439cd2a2fbe8daf780c2150c4c951d8e
|
||||
|
||||
[provide]
|
||||
spng = spng_dep
|
||||
112
tiff-tables.awk
Executable file
112
tiff-tables.awk
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/awk -f
|
||||
BEGIN {
|
||||
FS = ", *"
|
||||
print "// Generated by tiff-tables.awk. DO NOT MODIFY."
|
||||
print ""
|
||||
print "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
|
||||
print "#include <stddef.h>"
|
||||
print "#include <stdint.h>"
|
||||
print ""
|
||||
print "struct tiff_value {"
|
||||
print "\tconst char *name;"
|
||||
print "\tuint16_t value;"
|
||||
print "};"
|
||||
print ""
|
||||
print "struct tiff_entry {"
|
||||
print "\tconst char *name;"
|
||||
print "\tuint16_t tag;"
|
||||
print "\tstruct tiff_value *values;"
|
||||
print "};"
|
||||
print "#endif"
|
||||
}
|
||||
|
||||
{
|
||||
# Remember and strip consecutive comments.
|
||||
if (match($0, /#/))
|
||||
comment[++comments] = substr($0, RSTART + 1)
|
||||
else if (!/[[:space:]]/)
|
||||
comments = 0
|
||||
|
||||
sub(/#.*$/, "")
|
||||
sub(/[[:space:]]*$/, "")
|
||||
}
|
||||
|
||||
# Converts arbitrary strings to C identifiers (when running in the C locale).
|
||||
function identifize(s) {
|
||||
# Regard parenthesised text as comments.
|
||||
while (match(s, /[[:space:]]\([^)]+\)/)) {
|
||||
comment[++comments] = substr(s, RSTART, RLENGTH)
|
||||
s = substr(s, 1, RSTART - 1) substr(s, RSTART + RLENGTH)
|
||||
}
|
||||
|
||||
# Capitalize words (toupper is POSIX), removing spaces and dashes between.
|
||||
while (match(s, /[-[:space:]]./)) {
|
||||
s = substr(s, 1, RSTART - 1) \
|
||||
toupper(substr(s, RSTART + 1, 1)) \
|
||||
substr(s, RSTART + RLENGTH)
|
||||
}
|
||||
|
||||
# Replace any remaining non-identifier characters with underscores.
|
||||
gsub(/[^[:alnum:]]/, "_", s)
|
||||
return s
|
||||
}
|
||||
|
||||
function flushcomments(prefix, i, acc) {
|
||||
for (i = 1; i <= comments; i++)
|
||||
acc = acc prefix comment[i] "\n"
|
||||
comments = 0
|
||||
return acc
|
||||
}
|
||||
|
||||
function flushvalues() {
|
||||
if (values) {
|
||||
allvalues = allvalues "enum " fieldname " {\n" values "};\n\n"
|
||||
values = ""
|
||||
fields = fields "\n\t\t{}\n\t}},"
|
||||
} else if (fields) {
|
||||
fields = fields " NULL},"
|
||||
}
|
||||
}
|
||||
|
||||
function flushsection() {
|
||||
if (section) {
|
||||
flushvalues()
|
||||
print "};\n\n" allvalues "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
|
||||
print "static struct tiff_entry " \
|
||||
sectionsnakecase "_entries[] = {" fields "\n\t{}\n};"
|
||||
print "#endif"
|
||||
}
|
||||
}
|
||||
|
||||
# Section marker
|
||||
/^= / {
|
||||
flushsection()
|
||||
section = identifize(substr($0, 3))
|
||||
sectionsnakecase = tolower(substr($0, 3))
|
||||
gsub(/[^[:alnum:]]/, "_", sectionsnakecase)
|
||||
fields = ""
|
||||
allvalues = ""
|
||||
print "\n" flushcomments("//") "enum {"
|
||||
}
|
||||
|
||||
# Field
|
||||
section && /^[^\t=]/ {
|
||||
flushvalues()
|
||||
fieldname = section "_" identifize($2)
|
||||
fields = fields "\n\t{\"" $2 "\", " fieldname ","
|
||||
print flushcomments("\t//") "\t" fieldname " = " $1 ","
|
||||
}
|
||||
|
||||
# Value
|
||||
section && /^\t/ {
|
||||
sub(/^\t*/, "")
|
||||
valuename = fieldname "_" identifize($2)
|
||||
if (!values)
|
||||
fields = fields " (struct tiff_value[]) {"
|
||||
values = values flushcomments("\t//") "\t" valuename " = " $1 ",\n"
|
||||
fields = fields "\n\t\t{\"" $2 "\", " valuename "},"
|
||||
}
|
||||
|
||||
END {
|
||||
flushsection()
|
||||
}
|
||||
471
tiff-tables.db
Normal file
471
tiff-tables.db
Normal file
@@ -0,0 +1,471 @@
|
||||
# Use tiff-tables.awk to produce a C source file from this database.
|
||||
|
||||
# Use the Internet Archive should any of these links go down.
|
||||
#
|
||||
# TIFF Revision 6.0 (1992)
|
||||
# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFF6.pdf
|
||||
#
|
||||
# TIFF Technical Note 1: TIFF Trees (1993)
|
||||
# https://download.osgeo.org/libtiff/old/TTN1.ps
|
||||
#
|
||||
# DRAFT TIFF Technical Note 2 (1995)
|
||||
# https://www.awaresystems.be/imaging/tiff/specification/TIFFTechNote2.txt
|
||||
#
|
||||
# Adobe PageMaker 6.0 TIFF Technical Notes (1995) [includes TTN1]
|
||||
# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFPM6.pdf
|
||||
#
|
||||
# Adobe Photoshop TIFF Technical Notes (2002)
|
||||
# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFphotoshop.pdf
|
||||
# https://www.alternatiff.com/resources/TIFFphotoshop.pdf
|
||||
# - Note that ImageSourceData 8BIM frames are specified differently
|
||||
# from how Adobe XMP Specification Part 3 defines them.
|
||||
# - The document places a condition on SubIFDs, without further explanation.
|
||||
#
|
||||
# Adobe Photoshop TIFF Technical Note 3 (2005)
|
||||
# http://chriscox.org/TIFFTN3d1.pdf
|
||||
#
|
||||
# Exif Version 2.3 (2012)
|
||||
# https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf
|
||||
#
|
||||
# Exif Version 2.32 (2019)
|
||||
# https://www.cipa.jp/e/std/std-sec.html
|
||||
#
|
||||
# ISO/DIS 12234-2 (TIFF/EP) (2000-06-21)
|
||||
# http://www.barrypearson.co.uk/top2009/downloads/TAG2000-22_DIS12234-2.pdf
|
||||
#
|
||||
# Digital Negative (DNG) Specification 1.5.0.0 (2019)
|
||||
# https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.5.0.0.pdf
|
||||
#
|
||||
# CIPA DC-007-2021 (Multi-Picture Format)
|
||||
# https://www.cipa.jp/e/std/std-sec.html
|
||||
|
||||
# TIFF 6.0
|
||||
= TIFF
|
||||
254, NewSubfileType
|
||||
255, SubfileType
|
||||
1, Full-resolution image data
|
||||
2, Reduced-resolution image data
|
||||
3, Page of a multi-page image
|
||||
256, ImageWidth
|
||||
257, ImageLength
|
||||
258, BitsPerSample
|
||||
259, Compression
|
||||
1, Uncompressed
|
||||
2, CCITT 1D
|
||||
3, Group 3 Fax
|
||||
4, Group 4 Fax
|
||||
5, LZW
|
||||
6, JPEG
|
||||
7, JPEG datastream # DRAFT TIFF Technical Note 2 + TIFFphotoshop.pdf
|
||||
8, Deflate/zlib # Adobe Photoshop TIFF Technical Notes
|
||||
32773, PackBits
|
||||
32946, Deflate # Adobe Photoshop TIFF Technical Notes
|
||||
262, PhotometricInterpretation
|
||||
0, WhiteIsZero
|
||||
1, BlackIsZero
|
||||
2, RGB
|
||||
3, RGB Palette
|
||||
4, Transparency mask
|
||||
5, CMYK
|
||||
6, YCbCr
|
||||
8, CIELab
|
||||
9, ICCLab # Adobe PageMaker 6.0 TIFF Technical Notes
|
||||
32803, Color filter array # DIS/ISO 12234-2 + DNG 1.5.0.0
|
||||
34892, LinearRaw # DNG 1.5.0.0
|
||||
263, Threshholding
|
||||
1, No dithering or halftoning
|
||||
2, Ordered dither or halftoning
|
||||
3, Randomized process
|
||||
264, CellWidth
|
||||
265, CellLength
|
||||
266, FillOrder
|
||||
1, MSB-first
|
||||
2, LSB-first
|
||||
269, DocumentName
|
||||
270, ImageDescription
|
||||
271, Make
|
||||
272, Model
|
||||
273, StripOffsets
|
||||
274, Orientation
|
||||
1, TopLeft
|
||||
2, TopRight
|
||||
3, BottomRight
|
||||
4, BottomLeft
|
||||
5, LeftTop
|
||||
6, RightTop
|
||||
7, RightBottom
|
||||
8, LeftBottom
|
||||
277, SamplesPerPixel
|
||||
278, RowsPerStrip
|
||||
279, StripByteCounts
|
||||
280, MinSampleValue
|
||||
281, MaxSampleValue
|
||||
282, XResolution
|
||||
283, YResolution
|
||||
284, PlanarConfiguration
|
||||
1, Chunky
|
||||
2, Planar
|
||||
285, PageName
|
||||
286, XPosition
|
||||
287, YPosition
|
||||
288, FreeOffsets
|
||||
289, FreeByteCounts
|
||||
290, GrayResponseUnit
|
||||
1, 1/10
|
||||
2, 1/100
|
||||
3, 1/1000
|
||||
4, 1/10000
|
||||
5, 1/100000
|
||||
291, GrayResponseCurve
|
||||
292, T4Options
|
||||
293, T6Options
|
||||
296, ResolutionUnit
|
||||
1, None
|
||||
2, Inch
|
||||
3, Centimeter
|
||||
297, PageNumber
|
||||
301, TransferFunction
|
||||
305, Software
|
||||
306, DateTime
|
||||
315, Artist
|
||||
316, HostComputer
|
||||
317, Predictor
|
||||
1, None
|
||||
2, Horizontal
|
||||
3, Floating point # Adobe Photoshop TIFF Technical Note 3
|
||||
318, WhitePoint
|
||||
319, PrimaryChromaticities
|
||||
320, ColorMap
|
||||
321, HalftoneHints
|
||||
322, TileWidth
|
||||
323, TileLength
|
||||
324, TileOffsets
|
||||
325, TileByteCounts
|
||||
330, SubIFDs # TIFF Technical Note 1: TIFF Trees
|
||||
332, InkSet
|
||||
1, CMYK
|
||||
2, Non-CMYK
|
||||
333, InkNames
|
||||
334, NumberOfInks
|
||||
336, DotRange
|
||||
337, TargetPrinter
|
||||
338, ExtraSamples
|
||||
0, Unspecified
|
||||
1, Associated alpha
|
||||
2, Unassociated alpha
|
||||
339, SampleFormat
|
||||
1, Unsigned integer
|
||||
2, Two's complement signed integer
|
||||
3, IEEE floating-point
|
||||
4, Undefined
|
||||
340, SMinSampleValue
|
||||
341, SMaxSampleValue
|
||||
342, TransferRange
|
||||
343, ClipPath # TIFF Technical Note 2: Clipping Path
|
||||
344, XClipPathUnits # TIFF Technical Note 2: Clipping Path
|
||||
345, YClipPathUnits # TIFF Technical Note 2: Clipping Path
|
||||
346, Indexed # TIFF Technical Note 3: Indexed Images
|
||||
347, JPEGTables # DRAFT TIFF Technical Note 2 + TIFFphotoshop.pdf
|
||||
351, OPIProxy # Adobe PageMaker 6.0 TIFF Technical Notes
|
||||
512, JPEGProc
|
||||
1, Baseline sequential
|
||||
14, Lossless Huffman
|
||||
513, JPEGInterchangeFormat
|
||||
514, JPEGInterchangeFormatLength
|
||||
515, JPEGRestartInterval
|
||||
517, JPEGLosslessPredictors
|
||||
1, A
|
||||
2, B
|
||||
3, C
|
||||
4, A+B+C
|
||||
5, A+((B-C)/2)
|
||||
6, B+((A-C)/2)
|
||||
7, (A+B)/2
|
||||
518, JPEGPointTransforms
|
||||
519, JPEGQTables
|
||||
520, JPEGDCTables
|
||||
521, JPEGACTables
|
||||
529, YCbCrCoefficients
|
||||
530, YCbCrSubSampling
|
||||
531, YCbCrPositioning
|
||||
1, Centered
|
||||
2, Co-sited
|
||||
532, ReferenceBlackWhite
|
||||
700, XMP # Adobe XMP Specification Part 3 Table 12/13/39
|
||||
32781, ImageID # Adobe PageMaker 6.0 TIFF Technical Notes
|
||||
33421, CFARepeatPatternDim # DIS/ISO 12234-2
|
||||
33422, CFAPattern # DIS/ISO 12234-2
|
||||
33423, BatteryLevel # DIS/ISO 12234-2
|
||||
33432, Copyright
|
||||
# TODO(p): Extract IPTC DataSets, like we do directly with PSIRs.
|
||||
33723, IPTC # Adobe XMP Specification Part 3 Table 12/39
|
||||
# TODO(p): Extract PSIRs, like we do directly with the JPEG segment.
|
||||
34377, Photoshop # Adobe XMP Specification Part 3 Table 12/39
|
||||
34665, Exif IFD Pointer # Exif 2.3
|
||||
34853, GPS Info IFD Pointer # Exif 2.3
|
||||
37398, TIFF/EP StandardID # DIS/ISO 12234-2
|
||||
37399, SensingMethod # DIS/ISO 12234-2, similar to Exif 41495
|
||||
0, Undefined
|
||||
1, Monochrome area sensor
|
||||
2, One-chip color area sensor
|
||||
3, Two-chip color area sensor
|
||||
4, Three-chip color area sensor
|
||||
5, Color sequential area sensor
|
||||
6, Monochrome linear sensor
|
||||
7, Trilinear sensor
|
||||
8, Color sequential linear sensor
|
||||
# TODO(p): Add more TIFF/EP tags that can be only in IFD0.
|
||||
37724, ImageSourceData # Adobe Photoshop TIFF Technical Notes
|
||||
50706, DNGVersion # DNG 1.5.0.0
|
||||
50707, DNGBackwardVersion # DNG 1.5.0.0
|
||||
50708, UniqueCameraModel # DNG 1.5.0.0
|
||||
50709, LocalizedCameraModel # DNG 1.5.0.0
|
||||
# TODO(p): Add more DNG tags that can be only in IFD0.
|
||||
|
||||
# Exif 2.3 4.6.5
|
||||
= Exif
|
||||
33434, ExposureTime
|
||||
33437, FNumber
|
||||
34850, ExposureProgram
|
||||
0, Not defined
|
||||
1, Manual
|
||||
2, Normal program
|
||||
3, Aperture priority
|
||||
4, Shutter priority
|
||||
5, Creative program
|
||||
6, Action program
|
||||
7, Portrait mode
|
||||
8, Landscape mode
|
||||
34852, SpectralSensitivity
|
||||
34855, PhotographicSensitivity
|
||||
34856, OECF
|
||||
34864, SensitivityType
|
||||
0, Unknown
|
||||
1, Standard output sensitivity
|
||||
2, Recommended exposure index
|
||||
3, ISO speed
|
||||
4, SOS and REI
|
||||
5, SOS and ISO speed
|
||||
6, REI and ISO speed
|
||||
7, SOS and REI and ISO speed
|
||||
34865, StandardOutputSensitivity
|
||||
34866, RecommendedExposureIndex
|
||||
34867, ISOSpeed
|
||||
34868, ISOSpeedLatitudeyyy
|
||||
34869, ISOSpeedLatitudezzz
|
||||
36864, ExifVersion
|
||||
36867, DateTimeOriginal
|
||||
36868, DateTimeDigitized
|
||||
36880, OffsetTime # 2.31
|
||||
36881, OffsetTimeOriginal # 2.31
|
||||
36882, OffsetTimeDigitized # 2.31
|
||||
37121, ComponentsConfiguration
|
||||
0, Does not exist
|
||||
1, Y
|
||||
2, Cb
|
||||
3, Cr
|
||||
4, R
|
||||
5, G
|
||||
6, B
|
||||
37122, CompressedBitsPerPixel
|
||||
37377, ShutterSpeedValue
|
||||
37378, ApertureValue
|
||||
37379, BrightnessValue
|
||||
37380, ExposureBiasValue
|
||||
37381, MaxApertureValue
|
||||
37382, SubjectDistance
|
||||
37383, MeteringMode
|
||||
0, Unknown
|
||||
1, Average
|
||||
2, CenterWeightedAverage
|
||||
3, Spot
|
||||
4, MultiSpot
|
||||
5, Pattern
|
||||
6, Partial
|
||||
255, Other
|
||||
37384, LightSource
|
||||
0, Unknown
|
||||
1, Daylight
|
||||
2, Fluorescent
|
||||
3, Tungsten (incandescent light)
|
||||
4, Flash
|
||||
9, Fine weather
|
||||
10, Cloudy weather
|
||||
11, Shade
|
||||
12, Daylight fluorescent (D 5700 - 7100K)
|
||||
13, Day white fluorescent (N 4600 - 5500K)
|
||||
14, Cool white fluorescent (W 3800 - 4500K)
|
||||
15, White fluorescent (WW 3250 - 3800K)
|
||||
16, Warm white fluorescent (L 2600 - 3250K)
|
||||
17, Standard light A
|
||||
18, Standard light B
|
||||
19, Standard light C
|
||||
20, D55
|
||||
21, D65
|
||||
22, D75
|
||||
23, D50
|
||||
24, ISO studio tungsten
|
||||
255, Other light source
|
||||
37385, Flash
|
||||
37386, FocalLength
|
||||
37396, SubjectArea
|
||||
37500, MakerNote
|
||||
# TODO(p): Decode.
|
||||
37510, UserComment
|
||||
37520, SubSecTime
|
||||
37521, SubSecTimeOriginal
|
||||
37522, SubSecTimeDigitized
|
||||
37888, Temperature # 2.31
|
||||
37889, Humidity # 2.31
|
||||
37890, Pressure # 2.31
|
||||
37891, WaterDepth # 2.31
|
||||
37892, Acceleration # 2.31
|
||||
37893, CameraElevationAngle # 2.31
|
||||
40960, FlashpixVersion
|
||||
40961, ColorSpace
|
||||
1, sRGB
|
||||
65535, Uncalibrated
|
||||
40962, PixelXDimension
|
||||
40963, PixelYDimension
|
||||
40964, RelatedSoundFile
|
||||
40965, Interoperability IFD Pointer
|
||||
41483, FlashEnergy
|
||||
41484, SpatialFrequencyResponse
|
||||
41486, FocalPlaneXResolution
|
||||
41487, FocalPlaneYResolution
|
||||
41488, FocalPlaneResolutionUnit
|
||||
41492, SubjectLocation
|
||||
41493, ExposureIndex
|
||||
41495, SensingMethod
|
||||
1, Not defined
|
||||
2, One-chip color area sensor
|
||||
3, Two-chip color area sensor
|
||||
4, Three-chip color area sensor
|
||||
5, Color sequential area sensor
|
||||
7, Trilinear sensor
|
||||
8, Color sequential linear sensor
|
||||
41728, FileSource
|
||||
0, Others
|
||||
1, Scanner of transparent type
|
||||
2, Scanner of reflex type
|
||||
3, DSC
|
||||
41729, SceneType
|
||||
1, Directly-photographed image
|
||||
41730, CFAPattern
|
||||
41985, CustomRendered
|
||||
0, Normal process
|
||||
1, Custom process
|
||||
41986, ExposureMode
|
||||
0, Auto exposure
|
||||
1, Manual exposure
|
||||
2, Auto bracket
|
||||
41987, WhiteBalance
|
||||
0, Auto white balance
|
||||
1, Manual white balance
|
||||
41988, DigitalZoomRatio
|
||||
41989, FocalLengthIn35mmFilm
|
||||
41990, SceneCaptureType
|
||||
0, Standard
|
||||
1, Landscape
|
||||
2, Portrait
|
||||
3, Night scene
|
||||
41991, GainControl
|
||||
0, None
|
||||
1, Low gain up
|
||||
2, High gain up
|
||||
3, Low gain down
|
||||
4, High gain down
|
||||
41992, Contrast
|
||||
0, Normal
|
||||
1, Soft
|
||||
2, Hard
|
||||
41993, Saturation
|
||||
0, Normal
|
||||
1, Low
|
||||
2, High
|
||||
41994, Sharpness
|
||||
0, Normal
|
||||
1, Soft
|
||||
2, Hard
|
||||
41995, DeviceSettingDescription
|
||||
41996, SubjectDistanceRange
|
||||
0, Unknown
|
||||
1, Macro
|
||||
2, Close view
|
||||
3, Distant view
|
||||
42016, ImageUniqueID
|
||||
42032, CameraOwnerName
|
||||
42033, BodySerialNumber
|
||||
42034, LensSpecification
|
||||
42035, LensMake
|
||||
42036, LensModel
|
||||
42037, LensSerialNumber
|
||||
42080, CompositeImage # 2.32
|
||||
42081, SourceImageNumberOfCompositeImage # 2.32
|
||||
42082, SourceExposureTimesOfCompositeImage # 2.32
|
||||
42240, Gamma
|
||||
|
||||
# Exif 2.3 4.6.6 (Notice it starts at 0.)
|
||||
= Exif GPS
|
||||
0, GPSVersionID
|
||||
1, GPSLatitudeRef
|
||||
2, GPSLatitude
|
||||
3, GPSLongitudeRef
|
||||
4, GPSLongitude
|
||||
5, GPSAltitudeRef
|
||||
0, Sea level
|
||||
1, Sea level reference (negative value)
|
||||
6, GPSAltitude
|
||||
7, GPSTimeStamp
|
||||
8, GPSSatellites
|
||||
9, GPSStatus
|
||||
10, GPSMeasureMode
|
||||
11, GPSDOP
|
||||
12, GPSSpeedRef
|
||||
13, GPSSpeed
|
||||
14, GPSTrackRef
|
||||
15, GPSTrack
|
||||
16, GPSImgDirectionRef
|
||||
17, GPSImgDirection
|
||||
18, GPSMapDatum
|
||||
19, GPSDestLatitudeRef
|
||||
20, GPSDestLatitude
|
||||
21, GPSDestLongitudeRef
|
||||
22, GPSDestLongitude
|
||||
23, GPSDestBearingRef
|
||||
24, GPSDestBearing
|
||||
25, GPSDestDistanceRef
|
||||
26, GPSDestDistance
|
||||
27, GPSProcessingMethod
|
||||
28, GPSAreaInformation
|
||||
29, GPSDateStamp
|
||||
30, GPSDifferential
|
||||
0, Measurement without differential correction
|
||||
1, Differential correction applied
|
||||
31, GPSHPositioningError
|
||||
|
||||
# Exif 2.3 4.6.7 (Notice it starts at 1, and collides with GPS.)
|
||||
= Exif Interoperability
|
||||
1, InteroperabilityIndex
|
||||
|
||||
# CIPA DC-007-2021 5.2.3., 5.2.4. (But derive "field names" from "tag names".)
|
||||
= MPF
|
||||
45056, MP Format Version Number # MPFVersion
|
||||
45057, Number of Images # NumberOfImages
|
||||
45058, MP Entry # MPEntry
|
||||
45059, Individual Image Unique ID List # ImageUIDList
|
||||
45060, Total Number of Captured Frames # TotalFrames
|
||||
45313, MP Individual Image Number # MPIndividualNum
|
||||
45569, Panorama Scanning Orientation # PanOrientation
|
||||
45570, Panorama Horizontal Overlap # PanOverlap_H
|
||||
45571, Panorama Vertical Overlap # PanOverlap_V
|
||||
45572, Base Viewpoint Number # BaseViewpointNum
|
||||
45573, Convergence Angle # ConvergenceAngle
|
||||
45574, Baseline Length # BaselineLength
|
||||
45575, Divergence Angle # VerticalDivergence
|
||||
45576, Horizontal Axis Distance # AxisDistance_X
|
||||
45577, Vertical Axis Distance # AxisDistance_Y
|
||||
45578, Collimation Axis Distance # AxisDistance_Z
|
||||
45579, Yaw Angle # YawAngle
|
||||
45580, Pitch Angle # PitchAngle
|
||||
45581, Roll Angle # RollAngle
|
||||
356
tiffer.h
Normal file
356
tiffer.h
Normal 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;
|
||||
}
|
||||
5
tools/.gitignore
vendored
5
tools/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
/pnginfo
|
||||
/jpeginfo
|
||||
/tiffinfo
|
||||
/webpinfo
|
||||
/bmffinfo
|
||||
@@ -1,15 +0,0 @@
|
||||
SHELL = /bin/sh
|
||||
# libjq 1.6 lacks a pkg-config file, and there is no release in sight.
|
||||
# libjq 1.6 is required.
|
||||
CFLAGS = -g -O2 -Wall -Wextra `pkg-config --cflags $(deps)`
|
||||
LDLIBS = -ljq `pkg-config --libs $(deps)`
|
||||
|
||||
deps = libpng
|
||||
targets = pnginfo jpeginfo tiffinfo webpinfo bmffinfo
|
||||
|
||||
all: $(targets)
|
||||
$(targets): info.h
|
||||
clean:
|
||||
rm -f $(targets)
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -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
|
||||
142
tools/bmffinfo.c
142
tools/bmffinfo.c
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh -e
|
||||
# Remove thumbnails with URIs pointing to at this moment non-existing files.
|
||||
make pnginfo
|
||||
ninja pnginfo
|
||||
|
||||
pnginfo=$(pwd)/pnginfo cache_home=${XDG_CACHE_HOME:-$HOME/.cache}
|
||||
for size in normal large x-large xx-large; do
|
||||
|
||||
210
tools/hotpixels.c
Normal file
210
tools/hotpixels.c
Normal file
@@ -0,0 +1,210 @@
|
||||
//
|
||||
// hotpixels.c: look for hot pixels in raw image files
|
||||
//
|
||||
// Usage: pass a bunch of raw photo images taken with the lens cap on at,
|
||||
// e.g., ISO 8000-12800 @ 1/20-1/60, and store the resulting file as,
|
||||
// e.g., Nikon D7500.badpixels, which can then be directly used by Rawtherapee.
|
||||
//
|
||||
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include <libraw.h>
|
||||
|
||||
#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
|
||||
#error LibRaw 0.21.0 or newer is required.
|
||||
#endif
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void *
|
||||
xreallocarray(void *o, size_t n, size_t m)
|
||||
{
|
||||
if (m && n > SIZE_MAX / m) {
|
||||
fprintf(stderr, "xreallocarray: %s\n", strerror(ENOMEM));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
void *p = realloc(o, n * m);
|
||||
if (!p && n && m) {
|
||||
fprintf(stderr, "xreallocarray: %s\n", strerror(errno));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct coord { ushort x, y; };
|
||||
|
||||
static bool
|
||||
coord_equals(struct coord a, struct coord b)
|
||||
{
|
||||
return a.x == b.x && a.y == b.y;
|
||||
}
|
||||
|
||||
static int
|
||||
coord_cmp(const void *a, const void *b)
|
||||
{
|
||||
const struct coord *ca = (const struct coord *) a;
|
||||
const struct coord *cb = (const struct coord *) b;
|
||||
return ca->y != cb->y
|
||||
? (int) ca->y - (int) cb->y
|
||||
: (int) ca->x - (int) cb->x;
|
||||
}
|
||||
|
||||
struct candidates {
|
||||
struct coord *xy;
|
||||
size_t len;
|
||||
size_t alloc;
|
||||
};
|
||||
|
||||
static void
|
||||
candidates_add(struct candidates *c, ushort x, ushort y)
|
||||
{
|
||||
if (c->len == c->alloc) {
|
||||
c->alloc += 64;
|
||||
c->xy = xreallocarray(c->xy, sizeof *c->xy, c->alloc);
|
||||
}
|
||||
|
||||
c->xy[c->len++] = (struct coord) {x, y};
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// A stretch of zeroes that is assumed to mean start of outliers.
|
||||
#define SPAN 10
|
||||
|
||||
static const char *
|
||||
process_raw(struct candidates *c, const uint8_t *p, size_t len)
|
||||
{
|
||||
libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
|
||||
if (!iprc)
|
||||
return "failed to obtain a LibRaw handle";
|
||||
|
||||
int err = 0;
|
||||
if ((err = libraw_open_buffer(iprc, p, len)) ||
|
||||
(err = libraw_unpack(iprc))) {
|
||||
libraw_close(iprc);
|
||||
return libraw_strerror(err);
|
||||
}
|
||||
if (!iprc->rawdata.raw_image) {
|
||||
libraw_close(iprc);
|
||||
return "only Bayer raws are supported, not Foveon";
|
||||
}
|
||||
|
||||
// Make a histogram.
|
||||
uint64_t bins[USHRT_MAX] = {};
|
||||
for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
|
||||
for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
|
||||
ushort y = iprc->sizes.top_margin + yy;
|
||||
ushort x = iprc->sizes.left_margin + xx;
|
||||
bins[iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x]]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Detecting outliers is not completely straight-forward,
|
||||
// it may help to see the histogram.
|
||||
if (getenv("HOTPIXELS_HISTOGRAM")) {
|
||||
for (ushort i = 0; i < USHRT_MAX; i++)
|
||||
fprintf(stderr, "%u ", (unsigned) bins[i]);
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
// Go to the first non-zero pixel value.
|
||||
size_t last = 0;
|
||||
for (; last < USHRT_MAX; last++)
|
||||
if (bins[last])
|
||||
break;
|
||||
|
||||
// Find the last pixel value we assume to not be hot.
|
||||
for (; last < USHRT_MAX - SPAN - 1; last++) {
|
||||
uint64_t nonzero = 0;
|
||||
for (int i = 1; i <= SPAN; i++)
|
||||
nonzero += bins[last + i];
|
||||
if (!nonzero)
|
||||
break;
|
||||
}
|
||||
|
||||
// Store coordinates for all pixels above that value.
|
||||
for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
|
||||
for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
|
||||
ushort y = iprc->sizes.top_margin + yy;
|
||||
ushort x = iprc->sizes.left_margin + xx;
|
||||
if (iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x] > last)
|
||||
candidates_add(c, xx, yy);
|
||||
}
|
||||
}
|
||||
|
||||
libraw_close(iprc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static const char *
|
||||
do_file(struct candidates *c, const char *filename)
|
||||
{
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
if (!fp)
|
||||
return strerror(errno);
|
||||
|
||||
uint8_t *data = NULL, buf[256 << 10];
|
||||
size_t n, len = 0;
|
||||
while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
|
||||
data = xreallocarray(data, len + n, 1);
|
||||
memcpy(data + len, buf, n);
|
||||
len += n;
|
||||
}
|
||||
|
||||
const char *err = ferror(fp)
|
||||
? strerror(errno)
|
||||
: process_raw(c, data, len);
|
||||
|
||||
fclose(fp);
|
||||
free(data);
|
||||
return err;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
struct candidates c = {};
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char *filename = argv[i], *err = do_file(&c, filename);
|
||||
if (err) {
|
||||
fprintf(stderr, "%s: %s\n", filename, err);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
qsort(c.xy, c.len, sizeof *c.xy, coord_cmp);
|
||||
|
||||
// If it is detected in all passed photos, it is probably indeed bad.
|
||||
int count = 1;
|
||||
for (size_t i = 1; i <= c.len; i++) {
|
||||
if (i != c.len && coord_equals(c.xy[i - 1], c.xy[i])) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count == argc - 1)
|
||||
printf("%u %u\n", c.xy[i - 1].x, c.xy[i - 1].y);
|
||||
|
||||
count = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
286
tools/info.c
Normal file
286
tools/info.c
Normal 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;
|
||||
}
|
||||
1526
tools/info.h
1526
tools/info.h
File diff suppressed because it is too large
Load Diff
610
tools/jpeginfo.c
610
tools/jpeginfo.c
@@ -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
175
tools/rawinfo.c
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
133
tools/webpinfo.c
133
tools/webpinfo.c
@@ -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;
|
||||
}
|
||||
Submodule wuffs-mirror-release-c deleted from cc74cb4d30
31
xdg.c
31
xdg.c
@@ -17,11 +17,14 @@
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/// Add `element` to the `output` set. `relation` is a map of sets of strings
|
||||
/// defining is-a relations, and is traversed recursively.
|
||||
static void
|
||||
add_applying_transitive_closure(
|
||||
const gchar *element, GHashTable *relation, GHashTable *output)
|
||||
const char *element, GHashTable *relation, GHashTable *output)
|
||||
{
|
||||
// Stop condition.
|
||||
if (!g_hash_table_add(output, g_strdup(element)))
|
||||
@@ -45,34 +48,46 @@ char *
|
||||
get_xdg_home_dir(const char *var, const char *default_)
|
||||
{
|
||||
const char *env = getenv(var);
|
||||
if (env && *env == '/')
|
||||
if (env && g_path_is_absolute(env))
|
||||
return g_strdup(env);
|
||||
|
||||
#ifdef G_OS_WIN32
|
||||
return g_build_filename(g_get_home_dir(), default_, NULL);
|
||||
#else
|
||||
// The specification doesn't handle a missing HOME variable explicitly.
|
||||
// Implicitly, assuming Bourne shell semantics, it simply resolves empty.
|
||||
const char *home = getenv("HOME");
|
||||
return g_build_filename(home ? home : "", default_, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reïmplemented partly due to https://gitlab.gnome.org/GNOME/glib/-/issues/2501
|
||||
static gchar **
|
||||
get_xdg_data_dirs(void)
|
||||
{
|
||||
// GStrvBuilder is too new, it would help a little bit.
|
||||
GPtrArray *output = g_ptr_array_new_with_free_func(g_free);
|
||||
|
||||
#ifdef G_OS_WIN32
|
||||
g_ptr_array_add(output, g_strdup(g_get_user_data_dir()));
|
||||
for (const gchar *const *p = g_get_system_data_dirs(); *p; p++)
|
||||
g_ptr_array_add(output, g_strdup(*p));
|
||||
#else
|
||||
g_ptr_array_add(output, get_xdg_home_dir("XDG_DATA_HOME", ".local/share"));
|
||||
|
||||
const gchar *xdg_data_dirs;
|
||||
const char *xdg_data_dirs = "";
|
||||
if (!(xdg_data_dirs = getenv("XDG_DATA_DIRS")) || !*xdg_data_dirs)
|
||||
xdg_data_dirs = "/usr/local/share/:/usr/share/";
|
||||
|
||||
gchar **candidates = g_strsplit(xdg_data_dirs, ":", 0);
|
||||
gchar **candidates = g_strsplit(xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, 0);
|
||||
for (gchar **p = candidates; *p; p++) {
|
||||
if (**p == '/')
|
||||
if (g_path_is_absolute(*p))
|
||||
g_ptr_array_add(output, *p);
|
||||
else
|
||||
g_free(*p);
|
||||
}
|
||||
g_free(candidates);
|
||||
#endif
|
||||
g_ptr_array_add(output, NULL);
|
||||
return (gchar **) g_ptr_array_free(output, FALSE);
|
||||
}
|
||||
@@ -82,7 +97,7 @@ get_xdg_data_dirs(void)
|
||||
// Derived from shared-mime-info-spec 0.21.
|
||||
|
||||
static void
|
||||
read_mime_subclasses(const gchar *path, GHashTable *subclass_sets)
|
||||
read_mime_subclasses(const char *path, GHashTable *subclass_sets)
|
||||
{
|
||||
gchar *data = NULL;
|
||||
if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */))
|
||||
@@ -112,7 +127,7 @@ read_mime_subclasses(const gchar *path, GHashTable *subclass_sets)
|
||||
}
|
||||
|
||||
static gboolean
|
||||
filter_mime_globs(const gchar *path, guint is_globs2, GHashTable *supported_set,
|
||||
filter_mime_globs(const char *path, guint is_globs2, GHashTable *supported_set,
|
||||
GHashTable *output_set)
|
||||
{
|
||||
gchar *data = NULL;
|
||||
@@ -120,7 +135,7 @@ filter_mime_globs(const gchar *path, guint is_globs2, GHashTable *supported_set,
|
||||
return FALSE;
|
||||
|
||||
gchar *datasave = NULL;
|
||||
for (const gchar *line = strtok_r(data, "\r\n", &datasave); line;
|
||||
for (const char *line = strtok_r(data, "\r\n", &datasave); line;
|
||||
line = strtok_r(NULL, "\r\n", &datasave)) {
|
||||
if (*line == '#')
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user