Compare commits

...

119 Commits

Author SHA1 Message Date
karlch 464a1b7700 Add bumpversion config 2023-07-15 17:31:50 +02:00
karlch abd5f5bec7 Docs: Add v0.10.0 placeholder to changelog 2023-07-15 17:31:25 +02:00
karlch 16d9194a3a Release v0.9.0 2023-07-15 17:10:47 +02:00
Christian Karl f04a64feef
Merge pull request #657 from karlch/improve-docs
Improve docs
2023-07-15 11:06:41 +02:00
dependabot[bot] c360419b91
Bump actions/setup-python from 4.6.1 to 4.7.0 (#666)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.6.1 to 4.7.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4.6.1...v4.7.0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-15 11:02:02 +02:00
karlch cf4a1db957 metainfo.xml: Do not indent new releases
This will allow using bumpversion without having inconsistent
formatting.
2023-07-13 18:54:54 +02:00
karlch ea51251a2f metainfo.xml: Add missing release version 2023-07-13 18:54:54 +02:00
karlch 5e10abf0ad Fix edit detection for formats not supporting edit
In the buggy implementation, cropped always returned True in case the
current rect was empty due to the format not supporting editing.
2023-07-13 18:54:54 +02:00
Christian Karl 39f7be230a
Merge pull request #650 from jcjgraf/core/imageheader
Replace imghdr
2023-07-13 18:25:06 +02:00
karlch ff57aabfe9 Simple wording and typo changes 2023-07-13 18:16:46 +02:00
Christian Karl 2b5b2a64ee
Merge pull request #664 from karlch/suppress-qt-log
Disable qt logging at higher log levels
2023-07-12 22:09:21 +02:00
karlch 02b8f237c9 Disable qt logging at higher log levels
This allows us to also suppress various Qt messages.
2023-07-12 21:32:00 +02:00
karlch 0eab37e9ba Docs: Update changelog after dropping Python 3.7 2023-07-12 21:24:48 +02:00
Christian Karl 347079cd8d
Merge pull request #663 from karlch/drop-python37
Drop python37
2023-07-12 21:22:51 +02:00
karlch 135dc28d94 Drop note on using Protocols for _ModeWidget
Unfortunately using a Protocol is not enough as we need the _ModeWidget
to be both a QWidget and support the corresponding (Protocol) members.
Combining Protocols and regular inheritance is not possible, and there
currently is no option to combine types in an AND form (equivalent to
the OR form using typing.Union).
2023-07-12 21:16:23 +02:00
Christian Karl 526dde7b11
Merge pull request #662 from karlch/ci-with-black
CI: Replace pycodestyle with black
2023-07-12 21:10:43 +02:00
karlch ee4d0f00a0 Fix remaining black formatting issues 2023-07-12 20:21:17 +02:00
karlch b0a41a8381 CI: Replace pycodestyle with black 2023-07-12 20:12:59 +02:00
karlch 0212b52579 Fix typo and formatting in api.settings 2023-07-12 08:32:16 +02:00
Christian Karl 3ec77f04e2
Merge pull request #660 from buzzingwires/thumbnail_save
Add thumbnail.save configuration option.
2023-07-12 08:31:13 +02:00
Christian Karl facb142e50
Merge pull request #659 from Yutsuten/icon-cache
Docs: add information to update icons cache on manual install
2023-07-12 08:30:37 +02:00
karlch d06f9293e5 Update python and PyQt versions in checkversion 2023-07-11 22:24:10 +02:00
karlch 70b7c18a68 Docs: Require at least python 3.8 for installation 2023-07-11 22:23:04 +02:00
karlch 13a3d062d3 Tox: Remove PyQt 5.11 and 5.12 2023-07-11 22:22:13 +02:00
karlch 80840c0e79 Update setup.py requiring at least python 3.8 2023-07-11 22:21:52 +02:00
karlch 4c111d3f39 CI: Drop python 3.7 workflows 2023-07-11 22:20:47 +02:00
Christian Karl bef5d71867
Merge pull request #654 from karlch/new-website-theme
Use a new sphinx theme for the website
2023-07-11 21:13:26 +02:00
buzzingwires 3f287e8d15
Again, fix careless linting error. 2023-07-11 14:02:00 -04:00
buzzingwires fcf6cc8bac
Specify shared icon cache as location for thumbnail.save 2023-07-11 13:54:46 -04:00
buzzingwires f27e246290
Remove redundant configuration file reading tests. 2023-07-11 13:54:12 -04:00
Yutsuten 9381589d8f
Update documentation, revert Makefile 2023-07-11 21:36:46 +09:00
buzzingwires 0669eaff39
Fix careless lint error. 2023-07-11 01:46:49 -04:00
buzzingwires f2da422c80
Add thumbnail.save configuration option.
If this option is set to False, then do not save newly-generated
thumbnails to the disk. Previously saved thumbnail images will be
used normally and will never be deleted.

This saves space and reduces drive writes at the cost of extra
computational overhead by making the original image be reopened each
time the thumbnail is loaded.
2023-07-11 01:36:06 -04:00
dependabot[bot] 600301d44a
Bump tox from 4.6.3 to 4.6.4 in /misc/requirements (#658)
Bumps [tox](https://github.com/tox-dev/tox) from 4.6.3 to 4.6.4.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.6.3...4.6.4)

---
updated-dependencies:
- dependency-name: tox
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 18:16:47 +02:00
Yutsuten a20feaf700
Update icons cache 2023-07-08 14:22:32 +09:00
karlch 73e2b79fcc Docs: move command line argument page further back
Personally, I find commands and configuration more important to use
vimiv on a daily-basis.
2023-07-07 15:42:43 +02:00
karlch e023e05429 Docs: extend settings table
The settings table on the website now also includes type and default
value of settings. Required extending the rstutils script.
2023-07-07 15:42:43 +02:00
karlch 95e8a269f5 Simplify src2rst script
- Replace various map, lambda combinations with comprehensions
- Remove duplication
2023-07-07 15:42:43 +02:00
karlch 88bcd8a69a Tox: fix man page building with newer tox version 2023-07-07 15:42:43 +02:00
Jean-Claude cfb3b3a38e NEW: doc listing all CLI arguments and some examples
also improve the formatting of the man page by combining short- and
longform into brackets
2023-07-07 15:42:43 +02:00
Christian Karl 0f39a5569d
Merge pull request #655 from buzzingwires/none_sorting
Add the 'none' sorting setting
2023-07-07 14:14:50 +02:00
buzzingwires ab25bc1c60
Define link to profile. 2023-07-07 08:05:49 -04:00
buzzingwires f88a9b0cee
Rephrase documentation for `none` sort type.
Specify that it is mostly used for keeping the order from the
command line or stdin. Try to make the phrasing slightly more concise.
2023-07-07 06:28:02 -04:00
buzzingwires 8227419a34
Update changelog and AUTHORS 2023-07-07 06:19:22 -04:00
buzzingwires 8620bffada
Add the 'none' sorting setting
For sort 'image_order' and 'directory_order', the 'none' setting
can be used to allow images to follow the last order they were
encountered in by the software, instead of re-sorting.

Note that setting another sorting mode while the software is running
then switching back to 'none' will see files remain in the previous
sort order. The original order of images is *not* tracked.

Additionally, 'none' sort cannot be reversed.
2023-07-07 01:05:48 -04:00
karlch cbcc268815 Remove superfluous TODO comment
Raising the ValueError is the correct thing to do, and handled in all
cases.
2023-07-06 22:56:27 +02:00
karlch 2b11b08798 Remove pylint workaround in library enumerate
See https://github.com/PyCQA/pylint/issues/7963
2023-07-06 22:53:44 +02:00
karlch bca5d216d4 Update sphinx version and pin sphinx theme version 2023-07-06 13:54:00 +02:00
karlch 75e9bdb400 Docs: Use sphinxcontrib-images for nice galleries 2023-07-06 13:50:21 +02:00
karlch 4c9b942e32 Docs: Update screenshots 2023-07-06 11:25:03 +02:00
karlch da40fcc6a9 Docs: switch website theme to pydata
The sphinx bootstrap theme has been unmaintained / not updated for quite
a while. This theme works with more recent sphinx and has a few other
goodies:

- nice icons in the navbar
- dark / light mode
2023-07-06 11:24:08 +02:00
Christian Karl 0ceea493fc
Merge pull request #653 from karlch/ci-pyexiv2
Enable CI for pyexiv2
2023-07-05 19:36:05 +02:00
karlch d2c0112ef0 Remove break statement from pyexiv2
This causes the for loop to only fetch the first desired key.
2023-07-05 19:29:12 +02:00
karlch 2629abb771 CI: Use pyexiv2 workflow for python 3.8
We need piexif in addition as it is used in the tests to setup metadata
files.
2023-07-05 19:26:58 +02:00
Christian Karl 1aa8f194d6
Merge pull request #467 from jcjgraf/core/metadataplugin
Move Metadata Support to Pluginspace
2023-07-05 19:24:07 +02:00
karlch bff129bfda Docs: minor wording changes 2023-07-05 19:13:03 +02:00
Jean-Claude 816dc32b62
Update changelog to reflect this PR 2023-07-05 18:47:10 +02:00
Jean-Claude ebfd9d6806
Add note about enabling of multiple plugins 2023-07-05 18:47:10 +02:00
Jean-Claude 2d16f8942a
Add link to gexiv2 user metadata plugin 2023-07-05 18:47:10 +02:00
Jean-Claude 106281711d
Add url to metadata docs for missing metadata deps warning 2023-07-05 18:47:10 +02:00
Jean-Claude b9fb5dd379
Fix wrong url pointing to invalid exif page 2023-07-05 18:47:10 +02:00
Jean-Claude 2040326fbd
Add note about default loading behaviour 2023-07-05 18:47:10 +02:00
Jean-Claude c3c16543ae
Improve subsection titles
Also make them Title Case, as in other pasts of the docs
2023-07-05 18:47:10 +02:00
karlch 1cbba4a3a5
Refactor (metadata) plugin loading
User plugins are now loaded before app plugins, and not overridden. This
allows metadata (and any other app plugins) to be loaded later and
access any user-specific information. In this case, the user can
deactivate auto-loading by passing

metadata = none

in the plugins section of the config.
2023-07-05 16:13:51 +02:00
karlch db95c4b6ae
Fix typos 2023-07-05 16:06:28 +02:00
karlch 92669281de
Tests: Auto-register metadata backend markers 2023-07-05 16:06:10 +02:00
dependabot[bot] 715319db36
Bump pytest-mock from 3.10.0 to 3.11.1 in /misc/requirements (#644)
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.10.0 to 3.11.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.10.0...v3.11.1)

---
updated-dependencies:
- dependency-name: pytest-mock
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 19:14:38 +02:00
dependabot[bot] 4218da5fc0
Bump pytest from 7.3.1 to 7.4.0 in /misc/requirements (#638)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.1 to 7.4.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.3.1...7.4.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 19:05:14 +02:00
dependabot[bot] 9d024d8fb8
Bump pytest-xvfb from 2.0.0 to 3.0.0 in /misc/requirements (#645)
Bumps [pytest-xvfb](https://github.com/The-Compiler/pytest-xvfb) from 2.0.0 to 3.0.0.
- [Changelog](https://github.com/The-Compiler/pytest-xvfb/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/The-Compiler/pytest-xvfb/compare/v2.0.0...v3.0.0)

---
updated-dependencies:
- dependency-name: pytest-xvfb
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:30:22 +02:00
dependabot[bot] 07266c6f3a
Bump mypy from 1.2.0 to 1.4.1 in /misc/requirements (#649)
Bumps [mypy](https://github.com/python/mypy) from 1.2.0 to 1.4.1.
- [Commits](https://github.com/python/mypy/compare/v1.2.0...v1.4.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:29:42 +02:00
dependabot[bot] 60d0a5b54c
Bump pylint from 2.17.2 to 2.17.4 in /misc/requirements (#642)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.17.2 to 2.17.4.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.17.2...v2.17.4)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:29:22 +02:00
dependabot[bot] 40d8bc5073
Bump tox from 4.4.12 to 4.6.3 in /misc/requirements (#639)
Bumps [tox](https://github.com/tox-dev/tox) from 4.4.12 to 4.6.3.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.4.12...4.6.3)

---
updated-dependencies:
- dependency-name: tox
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:29:09 +02:00
dependabot[bot] 065265563e
Bump pytest-cov from 4.0.0 to 4.1.0 in /misc/requirements (#640)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.0.0 to 4.1.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:28:24 +02:00
dependabot[bot] 40ac6b6573
Bump actions/setup-python from 4.6.0 to 4.6.1 (#637)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4.6.0...v4.6.1)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:17:16 +02:00
dependabot[bot] be6079a826
Bump actions/checkout from 3.5.2 to 3.5.3 (#636)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.2 to 3.5.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.2...v3.5.3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:17:02 +02:00
dependabot[bot] 7a89ceedc4
Bump coverage from 7.2.3 to 7.2.7 in /misc/requirements (#635)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.3 to 7.2.7.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.2.3...7.2.7)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 18:16:31 +02:00
Christian Karl a3acdac2e6
Merge pull request #648 from Yutsuten/boolean-toggle-documentation
Improve the documentation
2023-06-30 12:52:18 +02:00
Yutsuten dd2b25863a
Set syntax doc: set name[!] [[+|-]value], fix typo 2023-06-30 19:35:52 +09:00
Yutsuten e4c0877435
Fix lint 2023-06-28 23:45:47 +09:00
Yutsuten 1475b64d12
Update documentation about toggling bool setting 2023-06-28 23:28:57 +09:00
Jean-Claude fb66610094
Ensure backward compatibility but allow overwriting
If no metadata plugin has been specified, the default `metadata` plugins
loads one of `metadata_pyexiv` or `metadata_piexif` (depending on the
installed backend). If a specific backend is configured, they it is
ensured that `metadata` does not interfere.

This is achieved by deferring the loading of the `metadata` plugin to
the end of all plugins. At that point `metadata` can check if another
metadata plugin has been loaded, and if so, do nothing.
2023-06-24 15:51:16 +02:00
Jean-Claude a22982fb7f
Remove broken link
I did not replace the link with one to `metadata.rst` as this lead to
a weird error. I also do not think that this link is super relevant.
2023-06-24 15:02:45 +02:00
Jean-Claude 18208132c8
Cleaup obsolete comments 2023-06-24 14:47:06 +02:00
Jean-Claude 7b3e2ee10f
Fix piexif data not displayed in widget
The dict returned by `get_metadata` was keyed by the *truncated*
metadata key, and not the original one. As the metadata widget uses the
original keys to read out the data, nothing was displayed if long keys
are used in the config.
2023-06-24 14:00:52 +02:00
Jean-Claude 37d98097d5
Update docs to reflect metadata to plugin space transition 2023-06-24 14:00:52 +02:00
Jean-Claude aa6a80a924
Make widget displaying images in the requested order
Make sure that the extracted metadata is displayed in the same order, as
specified in the config.
2023-06-24 14:00:52 +02:00
Jean-Claude 89dd893a0e
Assert earlier if piexif is available
Assertion in `add_exif_inforamtion_bdd` is not reached when piexif is
not available. The fixture `exif_content` fails before with an
AttributeError NoneType.
2023-06-24 14:00:52 +02:00
Jean-Claude 409c80459e
Change metadat_pyexiv to require full keys
Remove backward compatibility: Someone having the old piexif keys
defined in their config, but wanting to use the pyexiv2 backend.
Maintaining this backward compatibility required ugly guessing of
potential key prefixes.
2023-06-24 14:00:52 +02:00
Jean-Claude 1132667948
Make all MetadataHandle methods raise exceptions
Changed the `UnsupportedMetadataOperation` exception to a more general
`MetadataError`. It gets called when a `MetadataHandler` method is called
without any metadata plugin registered, or if none of the registered one
provides support for `get_date_time`/`copy_metadata`.
2023-06-24 14:00:52 +02:00
Jean-Claude 5507de9c70
Cleanup overcomplicated expression 2023-06-24 14:00:52 +02:00
Jean-Claude 03fc90f408
Import full `abc` for consistency reason 2023-06-24 14:00:52 +02:00
Jean-Claude b3206b8e62
Cleanup of supression of exception which is never raised
`get_metadata/get_keys` do not raise NotImplementedError. This is a
leftover from before refactoring MetadataPlugin into an abstract class
2023-06-24 14:00:52 +02:00
Jean-Claude 0aa4f8b7f9
Change name/version property of MetadataPlugin to static method
While a property is well suited to get this data, having to call these
properties on an instance of MetadataPlugin does not really make sense.
While it is possible to implement classproperties, `abc` cannot enforce
their implementation out-of-the-box.
Therefore, the properties were changed to static methods.
2023-06-24 14:00:52 +02:00
karlch 4448981923
Fix typo: iff -> if 2023-06-24 14:00:51 +02:00
karlch a0fcb6b0b9
Fix lint / mypy 2023-06-24 14:00:51 +02:00
karlch 1555d20a05
Extend metadata integration test and markers
- Simple tests to check if piexif / pyexiv2 correctly register the
  metadata handler
- Correct skipping of fixtures with pytest.mark.piexif /
  pytest.mark.pyexiv2
2023-06-24 14:00:51 +02:00
karlch 01fa41d07e
Add metadata wrapper plugin
This allows loading metadata support by default, as is the current
behaviour. We can consider deprecating this over time, but we should
definitely keep metadata support by default as it is currently the case
for now.
2023-06-24 14:00:51 +02:00
karlch 99d29adeb1
Fix bug in piexif's copy_metadata implementation
The metadata is stored in self._metadata, while metadata is the actual
module.
2023-06-24 14:00:51 +02:00
karlch 53e12e70c0
Update metadatawidget end2end test
Avoid importing the widget without metadata support as this registers
the :metadata command.
2023-06-24 14:00:51 +02:00
karlch 8a93af623f
Tests: rewrite metadata markers
Unfortunately using metadata.has_metadata_support() does not work this
early in the code, as we import conftest much before loading plugins and
actually registering the metadata handlers.
2023-06-24 14:00:51 +02:00
karlch 56c88dfea7
Remove metadata information from --version
As these are now plugins, they are not available this early in the
loading process. Instead plugin information should be added in :version
explicitly.
2023-06-24 14:00:51 +02:00
karlch 65fcb20ccc
Add plugins_loaded signal to api.signals
The signal is emitted when plugins have been loaded. This allows the
mainwindow to initialize the metadatawidget afterwards, and only if
metadata related plugins have been loaded.
2023-06-24 14:00:51 +02:00
karlch 6bc17428d8
Fix end2end test markers: exif -> metadata 2023-06-24 14:00:51 +02:00
karlch fd6f951ad9
Remove metadata-related version unit tests
With the new plugin structure, adding this information directly to
--version is no longer simple and up for discussion.
2023-06-24 14:00:51 +02:00
karlch 2d98a51d88
Temporarily shorten metadata (integration) test
As we now rely on a plugin infrastructure, the old unit test no longer
makes sense. Thus the test is now moved to integration and can be
expanded in the next steps.
2023-06-24 14:00:51 +02:00
karlch b610237ff0
Make piexif / pyexiv2 lazy imports optional
While they are absolutely required to run the plugin - and I thus
thought they could stay required - running the test suite without piexif
/ pyexiv2 is very messy with them not being optional. We need to be able
to compare to None there.
2023-06-24 14:00:50 +02:00
Jean-Claude 446f4bd588
Fix conftest for metadata redesign and test version
This is a first attempt on fixing the tests for the redesign of the
metadata handling:

- The metadata_markers (old exif_markers) have been adapted. However, I
  do not think that they make sense. For `metadata` and `nometadata` the
  condition only indicates whether the current instance of vimiv has
  metadata support (or not). For `piexif` and `pyexiv2`they indicate if
  the backend package is installed.

- In addition, having condition `metadata_pyexiv2.pyexiv2 is not None`
  fails for mark `pyexiv2`. I am not sure why that is the case.

- Fixtures `metadata_support`, `no_metadata_support`, `piexif` and
  `pyexiv2` have been added and implemented. They bring
  `metadata._registry` into the respective state, and make use of
  `reset_metadata_registration`, to undo the changes after each tests.

- Updated `test_version`. Am I required to set for each testcase the
  mark, as well as appropriated fixture (i.e. `@mark.piexif` and specify
  `test_XXX(piexif)`. This seems a bit tedious.
2023-06-24 14:00:50 +02:00
Jean-Claude e2e9c4a050
Add display of metadata plugin status to version 2023-06-24 14:00:50 +02:00
Jean-Claude 2bd2ded1dc
Improve: lazy import backend packages 2023-06-24 14:00:50 +02:00
Jean-Claude 23c8fd140e
Refactor MetadataPlugin to be an abstract class
Changing MetadataPlugin to a abstract class:
- Enforces the client to set both, `get_metadata` and `get_keys`
    - `copy_metadata` and `get_date_time` is optional to implement
- Enforces the client to set `name` and `version`
    - Both are now a property. Prevents the loading of plugins if not
      used.

Additional changes:
- Simplified `_registry` to be a simple list. There is no need for a
  dict. Also, the additional `_has_...` properties are no longer needed.
- Got rid of all module level `has_...` functions, and added
  `has_metadata_support` instead. Fine-grain control is not needed.
2023-06-24 14:00:50 +02:00
Jean-Claude f2af1ded70
Fix pyexiv2 plugin to conform to refactoring
Also rename plugin from py3exiv2 to pyexiv2 to prevent confusion.
2023-06-24 14:00:50 +02:00
Jean-Claude 824fe8e3ca
Fix various mypy and linter issues 2023-06-24 14:00:50 +02:00
Jean-Claude f0740e50ef
Refactor registry to make client subclass MetadataPlugin
Clients no longer register individual functions, but they subclass
`MetadataPlugin`, and overwrite whatever methods the plugin offers.
In addition, clients are required to set the `name` and `version`
field of `MetadataPlugin`.
The subclass is registered using the register function.
2023-06-24 14:00:50 +02:00
Jean-Claude 50cb5814fc
Rename `imutils.exif` to `imutils.metadata`
Renaming is done as this file handles all metadata related
functionalities and not just exif metadata.
2023-06-24 14:00:50 +02:00
Jean-Claude 26cf2b680e
Improve metadata handler by operations.
Consecutive calls using the same arguments are cached and served from
cache. This applies for the backend setup, get_metadata and get_keys, as
these are rather expensive calls.

This may be problematic if metadata is written to the image in-between
calls.
2023-06-24 14:00:50 +02:00
Jean-Claude 9fb1155ae7
Remove get_raw_md, cleanup and docstrings
- MetadataHandler no longer has two operations that distinguish between
formatted and raw metadata. Such a functionality can be added later if
needed.
- exif.Methods was renamed to exif.Operations
- Docstrings were added
- Minor fixes
2023-06-24 14:00:50 +02:00
Jean-Claude 6f3d885ca8
Fix missing renames from ExifHandler to MetadataHandler 2023-06-24 14:00:50 +02:00
Jean-Claude 9de230d0d5
Add convenience functions to determine metadata support
Module functions indicate whether any implementations for the
Methods have been registered, and hence, what functionality
MetadataHandler is able to provide.
2023-06-24 14:00:49 +02:00
Jean-Claude d66107d391
NEW: move metadata support to plugin space
This commit is still very VIP

- Clients can register implementations of the metadata handler using
  @exif.register(exif.Methods.XXX)
- For copy_metadata, get_raw_metadata, get_formatted_metadata, and
  get_keys all registered implementations are run, and the output
  combined, for get_date_time, only the last registered one is used
- Two internal plugins for metadata support, using piexif and py3exiv2
  respectively, were added
2023-06-24 14:00:43 +02:00
88 changed files with 1681 additions and 1074 deletions

18
.bumpversion.cfg Normal file
View File

@ -0,0 +1,18 @@
[bumpversion]
current_version = 0.9.0
commit = True
message = Release v{new_version}
tag = True
tag_name = v{new_version}
[bumpversion:file:vimiv/__init__.py]
serialize = ({major}, {minor}, {patch})
[bumpversion:file:misc/org.karlch.vimiv.qt.metainfo.xml]
search = <!-- Add new releases here -->
replace = <!-- Add new releases here -->
<release version="{new_version}" date="{now:%Y-%m-%d}"/>
[bumpversion:file:docs/changelog.rst]
search = (unreleased)
replace = ({now:%Y-%m-%d})

View File

@ -1,5 +1,5 @@
Contributing Guidelines
=======================
Contributing
============
You want to contribute to vimiv? Great! Every little help counts and is appreciated!

View File

@ -10,18 +10,12 @@ jobs:
strategy:
matrix:
include:
- python: "3.7"
toxenv: pyqt511
- python: "3.7"
toxenv: pyqt512
- python: "3.7"
toxenv: pyqt513
- python: "3.8"
toxenv: pyqt513
- python: "3.8"
toxenv: pyqt514
- python: "3.8"
toxenv: pyqt515
toxenv: pyqt515-piexif-pyexiv2-cov
- python: "3.9"
toxenv: pyqt513
- python: "3.9"
@ -43,7 +37,7 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v3.5.3
- uses: actions/cache@v3
with:
path: |
@ -52,7 +46,7 @@ jobs:
~/.cache/pip
key: "${{ matrix.toxenv }}_${{ hashFiles('misc/requirements/requirements*.txt') }}_${{ hashFiles('scripts/maybe_build_cextension.py') }}_${{ hashFiles('scripts/lint_tests.py') }}"
- name: Set up ${{ matrix.python-version }}
uses: actions/setup-python@v4.6.0
uses: actions/setup-python@v4.7.0
with:
python-version: "${{ matrix.python }}"
- name: Install dependencies
@ -60,6 +54,13 @@ jobs:
sudo apt-get update && sudo apt-get install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libxcb-shape0
python -m pip install --upgrade pip
pip install -r misc/requirements/requirements_tox.txt
- name: Install dependencies for pyexiv2
if: "contains(matrix.toxenv, 'pyexiv2')"
run: |
sudo apt-get install libexiv2-dev libboost-python-dev
sudo ln -s /usr/lib/x86_64-linux-gnu/libboost_python3.so /usr/lib/x86_64-linux-gnu/libboost_python${PY//./}.so
env:
PY: ${{ matrix.python-version }}
- name: Test with tox
run: |
tox -e "${{ matrix.toxenv }}"

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v3.5.3
- uses: actions/cache@v3
with:
path: |
@ -18,7 +18,7 @@ jobs:
~/.cache/pip
key: "ghpages_${{ hashFiles('misc/requirements/requirements*.txt') }}_${{ hashFiles('scripts/src2rst.py') }}"
- name: Set up python
uses: actions/setup-python@v4.6.0
uses: actions/setup-python@v4.7.0
with:
python-version: '3.x'
- name: Install tox

View File

@ -9,9 +9,9 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.5.2
- uses: actions/checkout@v3.5.3
- name: Set up Python
uses: actions/setup-python@v4.6.0
uses: actions/setup-python@v4.7.0
with:
python-version: '3.x'
- name: Install dependencies

View File

@ -3,6 +3,7 @@ Wolfgang Popp (woefe) <mail at wolfgang-popp dot de>
Ankur Sinha (sanjayankur31) <ankursinha at fedoraproject dot org>
Jean-Claude Graf (jcjgraf) <jeanggi90 at gmail dot com>
Mateus Etto (Yutsuten) <mateus dot etto at gmail dot com>
buzzingwires (buzzingwires) <buzzingwires at outlook dot com>
All contributors of non-trivial code are listed here. Please send an email to
<karlch at protonmail dot com> or open an issue / pull request if you feel like your

View File

@ -1,79 +1,54 @@
/* Underline headers with differently sized blue lines */
h1 {
border-bottom: solid 4px #1F7DE6;
html[data-theme="light"] {
--pst-color-primary: #3F8DB4;
--pst-color-info: #3F8DB4;
--pst-color-secondary: #7AACBC;
--pst-color-link-hover: #7AACBC;
--pst-color-warning: #c79537;
--pst-color-success: #4aa56f;
--pst-color-attention: #f44336;
--pst-color-danger: #f44336;
--pst-color-background: rgb(244, 243, 245);
--pst-color-on-background: rgb(244, 243, 245);
--pst-color-surface: rgb(234, 233, 235);
--pst-color-on-surface: rgb(214, 213, 215);
--pst-color-text-base: #1F1D21;
}
h2 {
border-bottom: solid 2px #1F7DE6;
html[data-theme="dark"] {
--pst-color-secondary: #9FE2F6;
--pst-color-link-hover: #9FE2F6;
--pst-color-primary: #89C3D4;
--pst-color-info: #89C3D4;
--pst-color-warning: #c79537;
--pst-color-success: #4aa56f;
--pst-color-attention: #f44336;
--pst-color-danger: #f44336;
--pst-color-background: #1F1D21;
--pst-color-on-background: #2b292d;
--pst-color-surface: #2e2c30;
--pst-color-on-surface: #444246;
--pst-color-text-base: #F4F3F6;
--pst-color-text-muted: rgb(208, 204, 212)
}
h3, h4, h5, h6 {
border-bottom: solid 1px #1F7DE6;
.bordered-image-light img {
border: 3px solid #3F8DB4;
margin: 4px;
margin-top: 0px;
margin-bottom: 16px;
}
/* Add a margin around images */
img {
margin: 8px;
}
/* But not in the navbar on the top */
.navbar img {
margin: 0px;
margin-right: 4px;
}
/* Change navbar color to blue */
.navbar {
background: #1861B3;
color: #FFFFFF;
border-bottom: solid 4px #1F7DE6;
}
.navbar-default .navbar-nav > li > a:hover,
.navbar-default .navbar-nav > li > a:focus {
background-color: #1F7DE6;
}
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus {
background-color: #1F7DE6;
}
.navbar-default .navbar-toggle:hover,
.navbar-default .navbar-toggle:focus {
background-color: #1F7DE6;
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .open > a:hover,
.navbar-default .navbar-nav > .open > a:focus {
background-color: #1F7DE6;
}
@media (max-width: 767px) {
.navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
background-color: #1F7DE6;
}
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
background-color: #1F7DE6;
}
}
/* Change info box color from purple to dark blue */
.alert-info {
background-color: #1861B3;
color: #FFFFFF;
}
.alert-info a {
color: #999999;
}
.alert-info a:hover {
color: #BBBBBB;
text-decoration: underline;
.bordered-image-dark img {
border: 3px solid #7AACBC;
margin: 4px;
margin-top: 0px;
margin-bottom: 16px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 564 KiB

BIN
docs/_static/scrots/manipulate_dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
docs/_static/scrots/manipulate_light.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

View File

@ -1,53 +0,0 @@
{% extends "!layout.html" %}
{# Add navigation for next and previous page to footer #}
{% block footer %}
<footer class="footer">
<div class="container">
<p class="pull-right">
{# Custom part starts here #}
{%- if prev %}
<a href="{{ prev.link|e }}" title="{{ _('Previous Chapter: ') + prev.title|striptags }}">
{%- if theme_bootstrap_version == "2" -%}<span class="icon-chevron-left visible-tablet"></span>{%- endif -%}
{%- if theme_bootstrap_version == "3" -%}<span class="glyphicon glyphicon-chevron-left visible-sm"></span>{%- endif -%}
<span class="hidden-sm hidden-tablet">{{ "&laquo;"|safe }} {{ prev.title|striptags|truncate(length=16, killwords=True) }}</span>
</a>
&nbsp;
&nbsp;
{%- endif %}
<a href="#">Back to top</a>
{%- if next %}
&nbsp;
&nbsp;
<a href="{{ next.link|e }}" title="{{ _('Next Chapter: ') + next.title|striptags }}">
{%- if theme_bootstrap_version == "2" -%}<span class="icon-chevron-right visible-tablet"></span>{%- endif -%}
{%- if theme_bootstrap_version == "3" -%}<span class="glyphicon glyphicon-chevron-right visible-sm"></span>{%- endif -%}
<span class="hidden-sm hidden-tablet">{{ next.title|striptags|truncate(length=16, killwords=True) }} {{ "&raquo;"|safe }}</span>
</a>
{%- endif %}
{# Custom part ends here #}
{% if theme_source_link_position == "footer" %}
<br/>
{% include "sourcelink.html" %}
{% endif %}
</p>
<p>
{%- if show_copyright %}
{%- if hasdoc('copyright') %}
{% trans path=pathto('copyright'), copyright=copyright|e %}&copy; <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}<br/>
{%- else %}
{% trans copyright=copyright|e %}&copy; Copyright {{ copyright }}.{% endtrans %}<br/>
{%- endif %}
{%- endif %}
{%- if last_updated %}
{% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}<br/>
{%- endif %}
{%- if show_sphinx %}
{% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx-doc.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %}<br/>
{%- endif %}
</p>
</div>
</footer>
{% endblock %}

View File

@ -1,5 +0,0 @@
{% extends "!navbar.html" %}
{# Disable search as it destroys the layout for smaller screens #}
{% block navbarsearch %}
{% endblock %}

View File

@ -3,7 +3,11 @@ Changelog
All notable changes to vimiv are documented in this file.
v0.9.0 (unreleased)
v0.10.0 (unreleased)
--------------------
v0.9.0 (2023-07-15)
-------------------
Added:
@ -89,6 +93,9 @@ Added:
the changes with ``<return>`` and reject them with ``<escape>``. The
``{transformation-info}`` status module displays the currently selected geometry of
the original image. Thanks `@Yutsuten`_ for reviving this!
* Add the ``none`` sorting type for the ``sort.image_order`` and ``sort.directory_order``
options, implemented by `@buzzingwires`_
* Add the ``thumbnail.save`` option, implemented by `@buzzingwires`_
Changed:
^^^^^^^^
@ -105,6 +112,14 @@ Changed:
supported by our testing framework, and 5.11 is out since July 2018. Code will likely
still work with these versions, but as it is no longer tested, there is no guarantee.
* The ``shuffle`` setting was moved into the ``sort`` group.
* Complete refactoring of metadata support. The handler functionality is moved out
to the plugin space, allowing for full flexibility in choosing a suitable backend. By
default, ``metadata_pyexiv2`` or ``metadata_piexif`` is loaded, if the respective
backend is installed. The default behaviour can be overridden by explicitly loading a
metadata plugin.
* Vimiv now requires at least Python 3.8 and thus PyQt 5.13.2.
* Qt logs of level warning / critical are now suppressed if the corresponding vimiv log
level is higher.
Fixed:
^^^^^^
@ -530,3 +545,4 @@ Initial release of the Qt version.
.. _@timsofteng: https://github.com/timsofteng
.. _@kAldown: https://github.com/kaldown
.. _@Yutsuten: https://github.com/Yutsuten
.. _@buzzingwires: https://github.com/buzzingwires

View File

@ -24,7 +24,6 @@
import os
import sys
import sphinx_bootstrap_theme
from sphinx.ext import autodoc
sys.path.insert(0, os.path.abspath(".."))
@ -39,7 +38,7 @@ sys.path.insert(0, os.path.abspath(".."))
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"]
extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinxcontrib.images"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@ -91,9 +90,7 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "bootstrap"
html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
html_sidebars = {}
html_theme = "pydata_sphinx_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@ -106,19 +103,26 @@ html_static_path = ["_static"]
#
html_logo = "_static/vimiv/vimiv.svg"
html_theme_options = {
"bootswatch_theme": "cosmo",
"navbar_title": "vimiv",
"navbar_pagenav": True,
"navbar_sidebarrel": False,
"navbar_links": [
("Docs", "documentation/index"),
("Install", "documentation/install"),
("Screenshots", "screenshots"),
("Changelog", "changelog"),
("Contributing", "documentation/contributing"),
("GitHub", "https://github.com/karlch/vimiv-qt", True),
"logo": {
"text": "vimiv",
},
"icon_links": [
{
"name": "GitHub",
"url": "https://github.com/karlch/vimiv-qt",
"icon": "fa-brands fa-square-github",
},
{
"name": "PyPI",
"url": "https://pypi.org/project/vimiv",
"icon": "fa-solid fa-box",
},
],
"source_link_position": "footer",
"navbar_start": ["navbar-logo"],
}
html_sidebars = {
"**": [],
}
# -- Options for HTMLHelp output ------------------------------------------
@ -127,31 +131,6 @@ html_theme_options = {
htmlhelp_basename = "vimivdoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, "vimiv.tex", "vimiv Documentation", "Christian Karl", "manual")
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
@ -167,24 +146,6 @@ man_pages = [
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"vimiv",
"vimiv Documentation",
author,
"vimiv",
"One line description of project.",
"Miscellaneous",
)
]
def setup(app):
app.add_css_file("custom.css")
ignore_separator = "^" + 88 * "-" + "$"

View File

@ -0,0 +1,45 @@
Command Line Arguments
======================
When starting vimiv form the command line you have the ability to pass a number of
different argument to vimiv.
Examples
--------
In the following we present a few use cases of command line arguments.
* Start in library view with the thumbnail grid displayed::
vimiv * --command "enter thumbnail" --command "enter library"
* Start in read-only mode. This prevents accidental modification (renaming, moving, editing etc.) of any images::
vimiv --set read_only true
* Change WM_CLASS_INSTANCE to identify a vimiv instance::
vimiv --qt-args "--name myVimivInstance"
* Print the last selected image to STDOUT when quitting::
vimiv --output "%"
* Use vimiv as *Rofi for Images* to make a selection from candidate images::
mySel=$(echo $myCand | vimiv --input --output "%m" --command "enter thumbnail")
* Print debug logs of your amazing plugin you are writing and of the `api._mark` module which does not behave as you are expecting::
vimiv --debug myAmazingPlugin api._mark
Command Line Arguments
----------------------
The general calling structure is:
.. include:: synopsis.rstsrc
The following is an exhaustive list of all available arguments:
.. include:: options.rstsrc

View File

@ -7,7 +7,7 @@ to the ``$XDG_CONFIG_HOME/vimiv/keys.conf`` on first start where
``$XDG_CONFIG_HOME`` is usually ``~/.config/`` if you have not updated it.
The configuration file is structured into sections. Each section corresponds to
the mode in which the keybindings arevalid. In each section the keybindings are
the mode in which the keybindings are valid. In each section the keybindings are
defined using ``keybinding : command to bind to``. Therefore ``f : fullscreen``
maps the ``f`` key to the ``fullscreen`` command. Special keys like space must
be wrapped in tags in the form of ``<space>`` to allow to differentiate them

View File

@ -56,9 +56,11 @@ directories. The ordering principle is defined by the ``sort.image_order`` and
natural Natural ordering by basename, i.e. image2.jpg comes before image11.jpg
recently-modified Ordering by modification time (``mtime``)
size Ordering by filesize, in bytes for images, in number of files for directories
none Do not sort or reverse. Use the existing order of the images (including that of a previous sort type). This is mostly for keeping the order from the command line or stdin.
========================= ===========
In addition, the ordering can be reversed using ``sort.reverse`` and the string-like
orderings (``alphabetical`` and ``natural``) can be made case-insensitive using
``sort.ignore_case``. When ``sort.shuffle`` is set, the image filelist is shuffled
regardless of the other orderings, but the library remains ordered.
``sort.ignore_case`` except for when the ``none`` ordering type is used.
When ``sort.shuffle`` is set, the image filelist is shuffled regardless of the other
orderings, but the library remains ordered.

View File

@ -1,66 +0,0 @@
Exif
====
Vimiv provides optional exif support if either `pyexiv2`_ or `piexif`_ is available. If
this is the case:
#. Exif metadata is automatically copied from source to destination when writing images
to disk.
#. The ``:metadata`` command and the corresponding ``i``-keybinding is available.
#. The ``{exif-date-time}`` statusbar module is available.
.. include:: pyexiv2.rst
Advantages of the different exif libraries
------------------------------------------
`Pyexiv2`_ is the more powerful of the two options. One large advantage is that it
supports not only JPEG and TIFF images, but most common file types. In addition,
with pyexiv2 ``:metadata`` formats exif data into human readable format, for example
``FocalLength: 5.0 mm`` where `piexif`_ would only give ``FocalLength: 5.0``. However,
given it is written as python bindings to the c++ api of `exiv2`_, the installation is
more involved compared to the pure python `piexif`_ module.
We recommend to use `pyexiv2`_ if the installation is not too involved on your system
and `piexif`_ as a fallback solution or in case you don't need the full power of
`pyexiv2`_ and prefer something more lightweight.
Moving from piexif to pyexiv2
-----------------------------
As pyexiv2 is the more powerful option compared to piexif, vimiv will prefer pyexiv2
over piexif. Therefore, to switch to pyexiv2 simply install it on your system and vimiv
will use it automatically. If you have defined custom metadata sets in your config, you
may have to adjust them to use the full path to any key. See the next section for more
information on this.
Customizing metadata keysets
----------------------------
You can configure the information displayed by the ``:metadata`` command by adding your
own key sets to the ``METADATA`` section in your configfile like this::
keys2 = Override,Second,Set
keys4 = New,Fourth,Set
where the values must be a comma-separated list of valid metadata keys.
In case you are using `pyexiv2`_ you can find a complete overview of valid keys on the
`exiv2 webpage <https://www.exiv2.org/metadata.html>`_. You can choose any of the exif
or IPTC keys. It is considered best-practice to use the full path to any key, e.g.
``Exif.Image.FocalLength``, but for convenience the short version of the key, e.g.
``FocalLength``, also works for the keys in ``Exif.Image`` or ``Exif.Photo``.
`Piexif`_ unfortunately always uses the short form of the key, i.e. everything that
comes after the last ``.`` character. In case you pass the full path, vimiv will remove
everything up to and including the last ``.`` character and match only the short form.
You can get a list of valid metadata keys for the current image using the
``:metadata-list-keys`` command.
.. _exiv2: https://www.exiv2.org/index.html
.. _pyexiv2: https://python3-exiv2.readthedocs.io
.. _piexif: https://pypi.org/project/piexif/

View File

@ -1,5 +1,5 @@
Documentation
=============
Docs
====
The following documents are available:
@ -10,7 +10,8 @@ The following documents are available:
getting_started
commands
configuration/index
exif
cl_options/index
metadata
contributing
migrating

View File

@ -81,6 +81,8 @@ should work.
.. include:: dependency_info.rst
.. include:: updating_icon_cache.rst
.. _install_using_tox:
Using Tox
@ -123,16 +125,15 @@ You can now launch vimiv by running::
Dependencies
------------
* `Python <http://www.python.org/>`_ 3.6 or newer with development extension
* `Qt <http://qt.io/>`_ 5.11.3 or newer
* `Python <http://www.python.org/>`_ 3.8 or newer with development extension
* `Qt <http://qt.io/>`_ 5.13.2 or newer
- QtCore / qtbase
- QtSvg (optional for svg support)
* `PyQt5 <http://www.riverbankcomputing.com/software/pyqt/intro>`_ 5.11.3 or newer
* `PyQt5 <http://www.riverbankcomputing.com/software/pyqt/intro>`_ 5.13.2 or newer
* `setuptools <https://pypi.python.org/pypi/setuptools/>`_ (for installation)
* `pyexiv2 <https://python3-exiv2.readthedocs.io>`_ (optional for exif support)
* `piexif <https://pypi.org/project/piexif/>`_ (optional alternative for exif support)
.. include:: pyexiv2.rst
* `pyexiv2 <https://python3-exiv2.readthedocs.io>`_ (optional for metadata support)
* `piexif <https://pypi.org/project/piexif/>`_ (optional alternative for metadata
support)
Package Names For Distributions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,144 @@
Metadata
========
Vimiv provides optional metadata support. If enabled, then:
#. The ``:metadata`` command and the corresponding ``i``-keybinding is available. It
displays the metadata of the current image.
#. The ``{exif-date-time}`` statusbar module is available. It displays the Exif
creation time of the current image.
#. Metadata is automatically copied from source to destination when writing images to
disk.
Vimiv provides full flexibility to users in terms of what metadata backend to use.
Each backend comes with their advantages and disadvantages, and each user has different
requirements, as well as different package support. Therefore, vimiv provides metadata
backends as independent plugins, that can be loaded as one desires. In addition, users
have the ability to extend vimiv's metadata capabilities using
:ref:`custom plugins<Plugins>`, as described in
:ref:`a later section<create_own_plugins>`.
Vimiv comes with two default plugins:
* ``metadata_piexif`` is based on `piexif`_
* ``metadata_pyexiv2`` is based on `pyexiv2`_
In addition, there are the following user metadata plugins available:
.. _user_metadata_plugins:
.. table:: Overview of user plugins
:widths: 20 80
===================================================================== ===========
Name Description
===================================================================== ===========
`metadata_gexiv2 <https://github.com/jcjgraf/vimiv_metadata-gexiv2>`_ Based on `gexiv2 <https://gitlab.gnome.org/GNOME/gexiv2>`_
===================================================================== ===========
Enable Support
--------------
Metadata plugins are loaded as any other vimiv plugin. To enable one of the default
plugins, simply list its name in the ``PLUGINS`` section of the configuration file. In
addition, you need to ensure that the required backend is installed on your system.
To enable a user metadata plugin, first you need to download it, and put it into the
plugin folder. Afterwards loading is equivalent to the default plugins.
For more information on how to load plugins, please refer to the
:ref:`plugin section<Plugins>`.
If no metadata plugin is specified, vimiv will load one of the two default plugins, if
the respective backend is installed. To disable this default behaviour, specify
``metadata = none`` in the ``PLUGINS`` section.
.. warning::
Default loading may be dropped in a future version. If you rely on metadata support,
we recommend to explicitly specify what backend you want.
.. note::
Multiple metadata plugins can be registered at the same time. If they use distinct
keys, the value of both is combined in the output of ``:metadata``.
Comparison of Different Plugins
-------------------------------
In short, ``metadata_pyexiv2`` is much more powerful than ``metadata_piexif``, though
the dependencies are also more involved to install.
.. table:: Comparison of the two libraries
:widths: 20 15 20 45
======================= =================== ==================== =====================================================================
PROPERTY ``metadata_piexif`` ``metadata_pyexiv2`` Note
======================= =================== ==================== =====================================================================
Backend `piexif`_ `pyexiv2`_
Exif Support True True pyexiv2 can potentially extract more data for the same image
ICMP Support False True
XMP Suppport False True
Output Formatting False True e.g. ``FNumber: 63/10`` vs ``FNumber: F6.3``
Supported File Types JPEG, TIFF Many common types
Ease of installation Simple More complicated pyexiv2 requires some dependencies including the C++ library `exiv2`_
======================= =================== ==================== =====================================================================
We recommend to use ``metadata_pyexiv2`` if the installation of `pyexiv2`_ is not too
involved on your system and ``metadata_piexif`` as a fallback solution or in case you
don't need the full power of `pyexiv2`_ and prefer something more lightweight.
Also consider the list of available
:ref:`user metadata plugins<user_metadata_plugins>`.
Customize Keysets
-----------------
You can configure the information displayed by the ``:metadata`` command by adding your
own key sets to the ``METADATA`` section in your configfile like this::
keys2 = someKey,anotherOne,lastOne
keys4 = newKey,oneMore
where the values must be a comma-separated list of valid metadata keys.
In case you are using `pyexiv2`_, you can find a complete overview of valid keys on the
`exiv2 webpage <https://www.exiv2.org/metadata.html>`_. You can choose any of the Exif,
IPTC, or XMP keys.
`Piexif`_ uses a simplified form of the key. It does not use the ``Group.Subgroup``
prefix, which is present in each of `pyexiv2`_'s keys. However, ``metadata_piexif``
automatically does this truncation, if the provided keys are in the long form.
The ``:metadata-list-keys`` command provides a list of all valid metadata keys, that
the currently loaded metadata plugins can read. This serves as an easy way to see what
keys are available in the image.
.. _create_own_plugins:
Create Own Plugins
------------------
One can extend vimiv's metadata capabilities by creating own metadata plugins. This is
useful if you want to use a different metadata backend.
The rough steps are the following:
#. Create a plugin, that implements the abstract class
``vimiv.imutils.metadata.MetadataPlugin``
#. Implement all required methods
#. Optionally, also implement the non-required methods
#. In the plugin's init function, register the plugin using
``vimiv.imutils.metadata.register``
Please see the default metadata plugins for an example implementation.
.. _exiv2: https://www.exiv2.org/index.html
.. _pyexiv2: https://python3-exiv2.readthedocs.io
.. _piexif: https://pypi.org/project/piexif/

View File

@ -1,3 +0,0 @@
.. warning::
There are multiple packages named `pyexiv2`. Make sure you install the right one.

View File

@ -0,0 +1,6 @@
.. note::
You may need to run ``gtk-update-icon-cache /usr/share/icons/hicolor`` (replace the
path if installing the icons somewhere else) after the install to get the vimiv icon
in your application launcher. It has been reported that the command was needed for
`wofi <https://hg.sr.ht/~scoopta/wofi>`_.

View File

@ -3,6 +3,12 @@
.. image:: _static/vimiv/vimiv_banner_800.png
:width: 400px
:align: center
:class: only-light
.. image:: _static/vimiv/vimiv_banner_darkmode_800.png
:width: 400px
:align: center
:class: only-dark
.. note::
@ -10,8 +16,7 @@
there are already many improvements compared to the `gtk version
<https://github.com/karlch/vimiv>`_. The old version is only recommended if you
require a more stable software. In case there is anything you miss here, please
open an `issue on github <https://github.com/karlch/vimiv-qt/issues/>`_. Check the
:ref:`roadmap` for more details.
open an `issue on github <https://github.com/karlch/vimiv-qt/issues/>`_.
.. include:: description.rst
@ -26,19 +31,47 @@ Screenshots
Light theme:
.. image:: _static/scrots/image_light.png
.. thumbnail:: _static/scrots/image_light.png
:width: 300px
:group: light-theme-screenshots-only-light
:class: bordered-image-light only-light
.. image:: _static/scrots/thumbnail_light.png
.. thumbnail:: _static/scrots/thumbnail_light.png
:width: 300px
:class: bordered-image-light only-light
:group: light-theme-screenshots-only-light
.. thumbnail:: _static/scrots/image_light.png
:width: 300px
:class: bordered-image-dark only-dark
:group: light-theme-screenshots-only-dark
.. thumbnail:: _static/scrots/thumbnail_light.png
:width: 300px
:class: bordered-image-dark only-dark
:group: light-theme-screenshots-only-dark
Dark theme:
.. image:: _static/scrots/library_dark.png
.. thumbnail:: _static/scrots/library_dark.png
:width: 300px
:class: bordered-image-light only-light
:group: dark-theme-screenshots-only-light
.. image:: _static/scrots/command_dark.png
.. thumbnail:: _static/scrots/command_dark.png
:width: 300px
:class: bordered-image-light only-light
:group: dark-theme-screenshots-only-light
.. thumbnail:: _static/scrots/library_dark.png
:width: 300px
:class: bordered-image-dark only-dark
:group: dark-theme-screenshots-only-dark
.. thumbnail:: _static/scrots/command_dark.png
:width: 300px
:class: bordered-image-dark only-dark
:group: dark-theme-screenshots-only-dark
Contents
--------
@ -47,9 +80,10 @@ Contents
:maxdepth: 2
documentation/index
changelog
roadmap
documentation/install
screenshots
changelog
documentation/contributing
.. include:: documentation/getting_help.rst

View File

@ -1,12 +0,0 @@
.. _roadmap:
Roadmap
=======
In the current beta-phase, a minor release is planned roughly every month. After version
0.9 and once no major bugs are remaining, we can release version 1.0. This would then
replace the gtk version as default. Once this has happened, vimiv should strictly apply
`semantic versioning <https://semver.org/>`_.
The Gtk version will retrieve critical bugfixes at least until version 1.0 has been
released. No new features will be implemented there, though.

View File

@ -4,29 +4,86 @@ Screenshots
Dark Theme
^^^^^^^^^^^
.. image:: _static/scrots/image_dark.png
:width: 300px
.. thumbnail:: _static/scrots/image_dark.png
:group: dark-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. image:: _static/scrots/thumbnail_dark.png
:width: 300px
.. thumbnail:: _static/scrots/thumbnail_dark.png
:group: dark-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. image:: _static/scrots/library_dark.png
:width: 300px
.. thumbnail:: _static/scrots/library_dark.png
:group: dark-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. thumbnail:: _static/scrots/command_dark.png
:group: dark-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. thumbnail:: _static/scrots/image_dark.png
:group: dark-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark
.. thumbnail:: _static/scrots/thumbnail_dark.png
:group: dark-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark
.. thumbnail:: _static/scrots/library_dark.png
:group: dark-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark
.. thumbnail:: _static/scrots/command_dark.png
:group: dark-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark
.. image:: _static/scrots/command_dark.png
:width: 300px
Light Theme
^^^^^^^^^^^
.. image:: _static/scrots/image_light.png
:width: 300px
.. thumbnail:: _static/scrots/image_light.png
:group: light-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. image:: _static/scrots/thumbnail_light.png
:width: 300px
.. thumbnail:: _static/scrots/thumbnail_light.png
:group: light-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. image:: _static/scrots/library_light.png
:width: 300px
.. thumbnail:: _static/scrots/library_light.png
:group: light-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. image:: _static/scrots/command_light.png
:width: 300px
.. thumbnail:: _static/scrots/command_light.png
:group: light-theme-screenshots-only-light
:width: 400px
:class: bordered-image-light only-light
.. thumbnail:: _static/scrots/image_light.png
:group: light-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark
.. thumbnail:: _static/scrots/thumbnail_light.png
:group: light-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark
.. thumbnail:: _static/scrots/library_light.png
:group: light-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark
.. thumbnail:: _static/scrots/command_light.png
:group: light-theme-screenshots-only-dark
:width: 400px
:class: bordered-image-dark only-dark

View File

@ -44,14 +44,16 @@
<url type="help">https://karlch.github.io/vimiv-qt/documentation</url>
<update_contact>ankursinha AT fedoraproject.org</update_contact>
<releases>
<!-- Add new releases here -->
<release version="0.7.0" date="2020-05-17"/>
<release version="0.6.1" date="2020-03-07"/>
<release version="0.6.0" date="2020-03-07"/>
<release version="0.5.0" date="2020-01-05"/>
<release version="0.4.0" date="2019-12-01"/>
<release version="0.3.0" date="2019-11-01"/>
<release version="0.2.0" date="2019-10-01"/>
<release version="0.1.0" date="2019-08-15"/>
<!-- Add new releases here -->
<release version="0.9.0" date="2023-07-15"/>
<release version="0.8.0" date="2021-01-18"/>
<release version="0.7.0" date="2020-05-17"/>
<release version="0.6.1" date="2020-03-07"/>
<release version="0.6.0" date="2020-03-07"/>
<release version="0.5.0" date="2020-01-05"/>
<release version="0.4.0" date="2019-12-01"/>
<release version="0.3.0" date="2019-11-01"/>
<release version="0.2.0" date="2019-10-01"/>
<release version="0.1.0" date="2019-08-15"/>
</releases>
</component>

View File

@ -1,2 +1,2 @@
coverage==7.2.3
pytest-cov==4.0.0
coverage==7.2.7
pytest-cov==4.1.0

View File

@ -1,2 +1,3 @@
sphinx==5.3.0
sphinx-bootstrap-theme==0.8.1
sphinx==7.0.1
pydata-sphinx-theme==0.13.3
sphinxcontrib-images==0.9.4

View File

@ -1,3 +1,3 @@
pylint==2.17.2
pycodestyle==2.10.0
pylint==2.17.4
black==23.7.0
pydocstyle==6.3.0

View File

@ -1,2 +1,2 @@
mypy==1.2.0
mypy==1.4.1
PyQt5-stubs==5.15.6.0

View File

@ -1,6 +1,6 @@
flaky==3.7.0
pytest==7.3.1
pytest-mock==3.10.0
pytest==7.4.0
pytest-mock==3.11.1
pytest-qt==4.2.0
pytest-xvfb==2.0.0
pytest-xvfb==3.0.0
pytest-bdd==6.1.1

View File

@ -1 +1 @@
tox==4.4.12
tox==4.6.4

View File

@ -27,12 +27,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "VIMIV" "1" "Dec 26, 2021" "" "vimiv"
.TH "VIMIV" "1" "Jan 08, 2022" "" "vimiv"
.SH NAME
vimiv \- an image viewer with vim-like keybindings
.SH SYNOPSIS
.sp
\fBvimiv\fP [\fBPATH\fP] [\fB\-h\fP] [\fB\-\-help\fP] [\fB\-f\fP] [\fB\-\-fullscreen\fP] [\fB\-v\fP] [\fB\-\-version\fP] [\fB\-g\fP \fIWIDTHxHEIGHT\fP] [\fB\-\-geometry\fP \fIWIDTHxHEIGHT\fP] [\fB\-\-temp\-basedir\fP] [\fB\-\-config\fP \fIFILE\fP] [\fB\-\-keyfile\fP \fIFILE\fP] [\fB\-s\fP \fIOPTION\fP \fIVALUE\fP] [\fB\-\-set\fP \fIOPTION\fP \fIVALUE\fP] [\fB\-\-log\-level\fP \fILEVEL\fP] [\fB\-\-command\fP \fICOMMAND\fP] [\fB\-b\fP \fIDIRECTORY\fP] [\fB\-\-basedir\fP \fIDIRECTORY\fP] [\fB\-o\fP \fITEXT\fP] [\fB\-\-output\fP \fITEXT\fP] [\fB\-i\fP] [\fB\-\-input\fP] [\fB\-\-debug\fP \fIMODULE\fP] [\fB\-\-qt\-args\fP \fIARGS\fP]
\fBvimiv\fP [\fBPATH\fP] [\fB\-\-help\fP|\fB\-h\fP] [\fB\-\-fullscreen\fP|\fB\-f\fP] [\fB\-\-version\fP|\fB\-v\fP] [\fB\-\-geometry\fP \fIWIDTHxHEIGHT\fP|\fB\-g\fP \fIWIDTHxHEIGHT\fP] [\fB\-\-temp\-basedir\fP] [\fB\-\-config\fP \fIFILE\fP] [\fB\-\-keyfile\fP \fIFILE\fP] [\fB\-\-set\fP \fIOPTION\fP \fIVALUE\fP|\fB\-s\fP \fIOPTION\fP \fIVALUE\fP] [\fB\-\-log\-level\fP \fILEVEL\fP] [\fB\-\-command\fP \fICOMMAND\fP] [\fB\-\-basedir\fP \fIDIRECTORY\fP|\fB\-b\fP \fIDIRECTORY\fP] [\fB\-\-output\fP \fITEXT\fP|\fB\-o\fP \fITEXT\fP] [\fB\-\-input\fP|\fB\-i\fP] [\fB\-\-debug\fP \fIMODULE\fP] [\fB\-\-qt\-args\fP \fIARGS\fP]
.SH DESCRIPTION
.sp
Vimiv is an image viewer with vim\-like keybindings. It is written in python3
@ -54,7 +54,7 @@ Complete customization with style sheets
A much more complete documentation can be found under
\fI\%https://karlch.github.io/vimiv\-qt/\fP\&.
.SH OPTIONS
.SS positional arguments
.SS POSITIONAL ARGUMENTS
.sp
\fBPATH\fP
.INDENT 0.0
@ -62,30 +62,30 @@ A much more complete documentation can be found under
Paths to open
.UNINDENT
.UNINDENT
.SS optional arguments
.SS OPTIONS
.sp
\fB\-h\fP, \fB\-\-help\fP
\fB\-\-help\fP, \fB\-h\fP
.INDENT 0.0
.INDENT 3.5
show this help message and exit
.UNINDENT
.UNINDENT
.sp
\fB\-f\fP, \fB\-\-fullscreen\fP
\fB\-\-fullscreen\fP, \fB\-f\fP
.INDENT 0.0
.INDENT 3.5
Start fullscreen
.UNINDENT
.UNINDENT
.sp
\fB\-v\fP, \fB\-\-version\fP
\fB\-\-version\fP, \fB\-v\fP
.INDENT 0.0
.INDENT 3.5
Print version information and exit
.UNINDENT
.UNINDENT
.sp
\fB\-g\fP \fIWIDTHxHEIGHT\fP, \fB\-\-geometry\fP \fIWIDTHxHEIGHT\fP
\fB\-\-geometry\fP \fIWIDTHxHEIGHT\fP, \fB\-g\fP \fIWIDTHxHEIGHT\fP
.INDENT 0.0
.INDENT 3.5
Set the starting geometry
@ -113,7 +113,7 @@ Use FILE as keybinding file
.UNINDENT
.UNINDENT
.sp
\fB\-s\fP \fIOPTION\fP \fIVALUE\fP, \fB\-\-set\fP \fIOPTION\fP \fIVALUE\fP
\fB\-\-set\fP \fIOPTION\fP \fIVALUE\fP, \fB\-s\fP \fIOPTION\fP \fIVALUE\fP
.INDENT 0.0
.INDENT 3.5
Set a temporary setting
@ -134,27 +134,27 @@ Run COMMAND on startup, usable multiple times
.UNINDENT
.UNINDENT
.sp
\fB\-b\fP \fIDIRECTORY\fP, \fB\-\-basedir\fP \fIDIRECTORY\fP
\fB\-\-basedir\fP \fIDIRECTORY\fP, \fB\-b\fP \fIDIRECTORY\fP
.INDENT 0.0
.INDENT 3.5
Directory to use for all storage
.UNINDENT
.UNINDENT
.sp
\fB\-o\fP \fITEXT\fP, \fB\-\-output\fP \fITEXT\fP
\fB\-\-output\fP \fITEXT\fP, \fB\-o\fP \fITEXT\fP
.INDENT 0.0
.INDENT 3.5
Wildcard expanded string to print to standard output upon quit
.UNINDENT
.UNINDENT
.sp
\fB\-i\fP, \fB\-\-input\fP
\fB\-\-input\fP, \fB\-i\fP
.INDENT 0.0
.INDENT 3.5
Read paths to open from standard input
.UNINDENT
.UNINDENT
.SS development arguments
.SS DEVELOPMENT ARGUMENTS
.sp
\fB\-\-debug\fP \fIMODULE\fP
.INDENT 0.0
@ -175,6 +175,6 @@ Arguments to pass to qt directly, use quotes to pass multiple arguments
.SH AUTHOR
Christian Karl
.SH COPYRIGHT
2017-2021, Christian Karl
2017-2022, Christian Karl
.\" Generated by docutils manpage writer.
.

View File

@ -5,9 +5,9 @@ faulthandler_timeout = 30
markers =
current: Mark tests during development
imageformats: Require retrieving images from the web to test additional formats
exif: Require exif support
metadata: Require metadata support
piexif: Require piexif
pyexiv2: Require pyexiv2
noexif: Requires exif support NOT to be available
nometadata: Requires metadata support NOT to be available
ci: Run test only on ci
ci_skip: Skip test on ci

View File

@ -50,17 +50,24 @@ class RSTFile:
title: Title of the table.
"""
# Find out size of first column
length = max([len(elem[0]) for elem in rows])
ncols = len(rows[0])
lengths = [max([len(elem[i]) for elem in rows]) for i in range(ncols)]
empty_row = ["=" * n for n in lengths]
def format_row(row=empty_row):
parts = " ".join(f"{elem:{n}s}" for elem, n in zip(row, lengths))
return f" {parts}\n"
# Header
self.write(".. table:: %s\n :widths: %s\n\n" % (title, widths))
self.write(" %s ===========\n" % (length * "="))
self.write(" %-*s %s\n" % (length, rows[0][0], rows[0][1]))
self.write(" %s ===========\n" % (length * "="))
self.write(f".. table:: {title}\n :widths: {widths}\n\n")
self.write(format_row())
self.write(format_row(rows[0]))
self.write(format_row())
# Content
for row in rows[1:]:
self.write(" %-*s %s\n" % (length, row[0], row[1]))
self.write(format_row(row))
# Footer
self.write(" %s ===========\n" % (length * "="))
self.write(format_row())
def _write_header(self):
"""Write header to file explaining that the file was autogenerated."""

View File

@ -6,10 +6,13 @@
"""Generate reST documentation from source code docstrings."""
import collections
import inspect
import importlib
import os
import sys
import textwrap
from typing import Any, Dict, List, NamedTuple
# Startup is imported to create all the commands and keybindings via their decorators
from vimiv import api, parser, startup # pylint: disable=unused-import
@ -65,12 +68,15 @@ def generate_settings():
print("generating settings...")
filename = "docs/documentation/configuration/settings_table.rstsrc"
with RSTFile(filename) as f:
rows = [("Setting", "Description")]
rows = [("Setting", "Description", "Default", "Type")]
for name in sorted(api.settings._storage.keys()):
setting = api.settings._storage[name]
if setting.desc: # Otherwise the setting is meant to be hidden
rows.append((name, setting.desc))
f.write_table(rows, title="Overview of settings", widths="30 70")
default = str(setting.default)
if len(default) > 20: # Cut very long defaults (exif)
default = default[:17] + "..."
rows.append((name, setting.desc, default, setting.typ.__name__))
f.write_table(rows, title="Overview of settings", widths="20 60 15 5")
def generate_keybindings():
@ -93,105 +99,173 @@ def _gen_keybinding_rows(bindings):
def generate_commandline_options():
"""Generate file including the command line options."""
argparser = parser.get_argparser()
groups, titles = _get_options(argparser)
# Synopsis
arguments = _get_arguments(argparser)
# Synopsis Man Page
filename_synopsis = "docs/manpage/synopsis.rstsrc"
with open(filename_synopsis, "w", encoding="utf-8") as f:
synopsis_options = ["[%s]" % (title) for title in titles]
synopsis = "**vimiv** %s" % (" ".join(synopsis_options))
f.write(synopsis)
# Options
f.write(_generate_synopsis(arguments, emph="*", sep=r"\ \|\ "))
# Synopsis Documentation
filename_synopsis = "docs/documentation/cl_options/synopsis.rstsrc"
with open(filename_synopsis, "w", encoding="utf-8") as f:
raw_synopsis_doc = _generate_synopsis(arguments)
synopsis_doc = "\n ".join(textwrap.wrap(raw_synopsis_doc, width=79))
f.write(f".. code-block::\n\n {synopsis_doc}")
# Command Listing
groups = collections.defaultdict(list)
for arg in arguments:
groups[arg.group].append(arg)
# Command Listing Man Page
filename_options = "docs/manpage/options.rstsrc"
with RSTFile(filename_options) as f:
for title, argstr in groups.items():
f.write_section(title)
f.write(argstr)
_generate_cli_man(groups, f)
# Command Listing Documentation
filename_options = "docs/documentation/cl_options/options.rstsrc"
with RSTFile(filename_options) as f:
_generate_cli_doc(groups, f)
def _get_options(argparser):
"""Retrieve the options from the argument parser.
class ParserArgument(NamedTuple):
"""Storage class for a single command line argument."""
Returns:
groups: Dictionary of group titles and argument strings.
titles: List containing all argument titles.
"""
groups = {}
titles = []
for group in argparser._action_groups:
argstr = ""
for action in group._group_actions:
argstr += (
_format_positional(action, titles)
if "positional" in group.title
else _format_optional(action, titles)
)
groups[group.title] = argstr
return groups, titles
group: str
longname: str
shortname: str
metavar: Any
description: str
@property
def is_positional(self):
return self.longname is self.shortname is None
def get_names(self, formatter: str) -> List[str]:
"""Retrieve list of long and shortname which are not None."""
return [
self._format(name, formatter)
for name in [self.longname, self.shortname]
if name is not None
]
def get_metavar(self, formatter: str) -> str:
"""Retrieve metavar."""
if self.metavar is None:
return ""
if isinstance(self.metavar, tuple):
return " ".join(self._format(e, formatter) for e in self.metavar)
return self._format(self.metavar, formatter)
def get_name_metavar(
self, name_formatter: str, metavar_formatter: str
) -> List[str]:
"""Retrieve all not-none names and append metavar."""
names = self.get_names(name_formatter)
metavar = self.get_metavar(metavar_formatter)
lst = names if names else [metavar]
return [f"{e} {metavar}" if metavar else e for e in lst]
def _format(self, element: str, formatter: str) -> str:
"""Wrap the element into the format string."""
return formatter + element + formatter
def _format_optional(action, titles):
"""Format optional argument neatly.
def _get_arguments(argparser: parser.argparse.ArgumentParser) -> List[ParserArgument]:
"""Retrieve all arguments from the passed argparser.
Args:
action: The argparser action.
titles: List of titles to update with the title(s) of this argument.
argparser: Argument parser where the arguments get extracted from.
Returns:
Formatted string of this argument.
List of ParserArgument where each element represents one argument of the parser.
"""
_titles = _format_optional_title(action)
titles.extend(_titles)
title = ", ".join(_titles)
desc = action.help
return _format_option(title, desc)
def argument_from_action(group, action):
if len(action.option_strings) == 2:
shortname, longname = action.option_strings
elif len(action.option_strings) == 1:
longname = action.option_strings[0]
shortname = None
else:
longname = shortname = None
return ParserArgument(
group=group.title,
longname=longname,
shortname=shortname,
metavar=action.metavar,
description=action.help,
)
return [
argument_from_action(group, action)
for group in argparser._action_groups
for action in group._group_actions
]
def _format_positional(action, titles):
"""Format positional argument neatly.
def _generate_synopsis(arguments: List[ParserArgument], emph: str = "", sep="|") -> str:
"""Generate formatted synopsis of vimiv.
Args:
action: The argparser action.
titles: List of titles to update with the title of this argument.
arguments: List of instances of ParserArgument.
emph: Character used for emphasis.
sep: String used to separate different versions of the same argument, e.g. -o
and --output.
Returns:
Formatted string of this argument.
Formatted synopsis for the man page.
"""
title = "**%s**" % (action.metavar)
titles.append(title)
desc = action.help
return _format_option(title, desc)
emph2 = 2 * emph
synopsis = f"{emph2}vimiv{emph2}"
for arg in arguments:
if arg.is_positional:
synopsis += f" [{arg.get_metavar(emph2)}]"
else:
command = sep.join(arg.get_name_metavar(emph2, emph))
synopsis += f" [{command}]"
return synopsis
def _format_option(title, desc):
"""Format an option neatly.
def _generate_cli_man(groups: Dict[str, List[ParserArgument]], f: RSTFile) -> None:
"""Generate commands listing with man page formatting.
Args:
title: The title of this option.
desc: The help description of this option.
groups: List of ParserArgument sorted by their respective group.
f: RSTFile to write to.
"""
return "%s\n\n %s\n\n" % (title, desc)
for group in groups.keys():
arguments = groups[group]
f.write_section(group.upper())
for arg in arguments:
if arg.is_positional:
f.write(f"{arg.get_metavar('**')}\n\n\t{arg.description}\n\n")
else:
f.write(f"{', '.join(arg.get_name_metavar('**', '*'))}\n\n")
f.write(f"\t{arg.description}\n\n")
def _format_optional_title(action):
"""Format the title of an optional argument neatly.
The title depends on the number of arguments this action requires.
def _generate_cli_doc(groups: Dict[str, List[ParserArgument]], f: RSTFile) -> None:
"""Generate commands listing with documentation formatting.
Args:
action: The argparser action.
Returns:
Formatted title string of this argument.
groups: List of ParserArgument sorted by their respective group.
f: RSTFile to write to.
"""
formats = []
for option in action.option_strings:
if isinstance(action.metavar, str): # One argument
title = "**%s** *%s*" % (option, action.metavar)
elif isinstance(action.metavar, tuple): # Multiple arguments
elems = ["*%s*" % (elem) for elem in action.metavar]
title = "**%s** %s" % (option, " ".join(elems))
else: # No arguments
title = "**%s**" % (option)
formats.append(title)
return formats
for group in groups.keys():
arguments = groups[group]
rows = [("Command", "Description")]
for arg in arguments:
if arg.is_positional:
name = arg.get_metavar("``")
else:
name = ", ".join(f"``{e}``" for e in arg.get_name_metavar("", ""))
rows.append((name, arg.description))
f.write_table(rows, title=group.capitalize(), widths="50 50")
def generate_plugins():

View File

@ -38,8 +38,8 @@ def read_from_init(name):
setuptools.setup(
python_requires=">=3.6",
install_requires=["PyQt5>=5.9.2"],
python_requires=">=3.8",
install_requires=["PyQt5>=5.13.2"],
packages=setuptools.find_packages(),
ext_modules=[manipulate_module],
entry_points={"gui_scripts": ["vimiv = vimiv.startup:main"]},
@ -67,8 +67,6 @@ setuptools.setup(
"Natural Language :: English",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",

View File

@ -12,38 +12,36 @@ import urllib.request
import pytest
from vimiv.imutils import exif
from vimiv.plugins import metadata_pyexiv2, metadata_piexif
CI = "CI" in os.environ
HAS_PIEXIF = metadata_piexif.piexif is not None
HAS_PYEXIV2 = metadata_pyexiv2.pyexiv2 is not None
HAS_METADATA = HAS_PIEXIF or HAS_PYEXIV2
# fmt: off
PLATFORM_MARKERS = (
("ci", CI, "Only run on ci"),
("ci_skip", not CI, "Skipped on ci"),
)
EXIF_MARKERS = (
("exif", exif.has_exif_support, "Only run with exif support"),
("noexif", not exif.has_exif_support, "Only run without exif support"),
("pyexiv2", exif.pyexiv2 is not None, "Only run with pyexiv2"),
("piexif", exif.piexif is not None, "Only run with piexif"),
METADATA_MARKERS = (
("metadata", HAS_METADATA, "Only run with metadata support"),
("nometadata", not HAS_METADATA, "Only run without metadata support"),
("piexif", HAS_PIEXIF, "Only run with piexif"),
("pyexiv2", HAS_PYEXIV2, "Only run with pyexiv2"),
)
# fmt: on
def apply_platform_markers(item):
"""Apply markers that skip tests depending on the current platform."""
apply_markers_helper(item, PLATFORM_MARKERS)
def apply_exif_markers(item):
"""Apply markers that skip tests depending on specific exif support."""
if os.path.basename(item.fspath) in ("test_exif.py",):
for marker_name in "exif", "pyexiv2", "piexif":
marker = getattr(pytest.mark, marker_name)
def apply_fixture_markers(item, *names):
"""Helper function to mark all tests using specific fixtures with that mark."""
for name in names:
marker = getattr(pytest.mark, name)
if name in item.fixturenames:
item.add_marker(marker)
apply_markers_helper(item, EXIF_MARKERS)
def apply_markers_helper(item, markers):
@ -60,8 +58,9 @@ def apply_markers_helper(item, markers):
def pytest_collection_modifyitems(items):
"""Handle custom markers via pytest hook."""
for item in items:
apply_platform_markers(item)
apply_exif_markers(item)
apply_fixture_markers(item, "piexif", "pyexiv2")
apply_markers_helper(item, PLATFORM_MARKERS)
apply_markers_helper(item, METADATA_MARKERS)
@pytest.fixture
@ -165,28 +164,17 @@ def tmpdir():
raise ValueError("Use the 'tmp_path' fixture instead of 'tmpdir'")
@pytest.fixture()
def piexif(monkeypatch):
"""Pytest fixture to ensure only piexif is available."""
monkeypatch.setattr(exif, "pyexiv2", None)
@pytest.fixture()
def noexif(monkeypatch, piexif):
"""Pytest fixture to ensure no exif library is available."""
monkeypatch.setattr(exif, "piexif", None)
@pytest.fixture()
def add_exif_information():
"""Fixture to retrieve a helper function that adds exif content to an image."""
def add_exif_information_impl(path: str, content):
assert exif.piexif is not None, "piexif required to add exif information"
exif_dict = exif.piexif.load(path)
import piexif
exif_dict = piexif.load(path)
for ifd, ifd_dict in content.items():
for key, value in ifd_dict.items():
exif_dict[ifd][key] = value
exif.piexif.insert(exif.piexif.dump(exif_dict), path)
piexif.insert(piexif.dump(exif_dict), path)
return add_exif_information_impl

View File

@ -173,6 +173,13 @@ def start_vector_graphic(svg):
start([svg])
@bdd.given("I open an image and an animated gif")
def start_image_and_animated_gif(tmp_path, gif):
imagename = str(tmp_path / "image.png")
create_image(imagename)
start([imagename, gif])
@bdd.given("I open images from multiple directories")
def start_multiple_directories(tmp_path):
dir1 = tmp_path / "dir1"

View File

@ -8,6 +8,19 @@ import pytest
import pytest_bdd as bdd
from vimiv.parser import geometry
from vimiv.imutils import _ImageFileHandler
@pytest.fixture()
def file_handler():
"""Fixture to retrieve the current instance of the edit handler."""
return _ImageFileHandler.instance
@pytest.fixture()
def edit_handler(file_handler):
"""Fixture to retrieve the current instance of the edit handler."""
return file_handler._edit_handler
@bdd.then(bdd.parsers.parse("the image size should be {size}"))
@ -25,3 +38,8 @@ def ensure_size_not(size, image):
width_neq = expected.width() != image_rect.width()
height_neq = expected.height() != image_rect.height()
assert width_neq or height_neq
@bdd.then("the image should not be edited")
def ensure_not_edited(edit_handler):
assert not edit_handler.changed

View File

@ -41,3 +41,8 @@ Feature: Crop an image.
| 10 | -100 | 150x100+85+0 |
# Ignored as dx/dy are outside of the image
| 1000 | 1000 | 150x100+75+50 |
Scenario: Crop does not automatically consider a gif edited
Given I open an image and an animated gif
When I run next
Then the image should not be edited

View File

@ -19,6 +19,7 @@ except ImportError:
@pytest.fixture()
def exif_content():
assert piexif is not None, "piexif required create exif information."
return {
"0th": {
piexif.ImageIFD.Make: b"vimiv-testsuite",

View File

@ -8,6 +8,26 @@ Feature: Ordering the image filelist.
And the image number 2 should be image_2.jpg
And the image number 11 should be image_11.jpg
Scenario: Set none sorting after another sort
Given I open 12 images without leading zeros in their name
When I run set sort.image_order natural
Then the image should have the index 1
And the image number 1 should be image_1.jpg
And the image number 2 should be image_2.jpg
And the image number 11 should be image_11.jpg
When I run set sort.image_order none
Then the image should have the index 1
And the image number 1 should be image_1.jpg
And the image number 2 should be image_2.jpg
And the image number 11 should be image_11.jpg
Scenario: Reverse none sorting
Given I open 5 images
When I run set sort.image_order none
Then the image should have the index 1
When I run set sort.reverse
Then the image should have the index 1
Scenario: Reverse current filelist
Given I open 5 images
When I run set sort.reverse!

View File

@ -1,4 +1,4 @@
@exif
@metadata
Feature: Metadata widget displaying image exif information
Scenario: Show metadata widget

View File

@ -7,32 +7,36 @@
import pytest
import pytest_bdd as bdd
from vimiv.gui import metadatawidget
from vimiv.imutils import metadata
bdd.scenarios("metadata.feature")
@pytest.fixture
def metadata():
return metadatawidget.MetadataWidget.instance
def metadatawidget():
if metadata.has_metadata_support():
from vimiv.gui.metadatawidget import MetadataWidget
return MetadataWidget.instance
raise ValueError("No metadata support for metadata tests")
@bdd.then("the metadata widget should be visible")
def check_metadata_widget_visible(metadata):
assert metadata.isVisible()
def check_metadata_widget_visible(metadatawidget):
assert metadatawidget.isVisible()
@bdd.then("the metadata widget should not be visible")
def check_metadata_widget_not_visible(metadata):
assert not metadata.isVisible()
def check_metadata_widget_not_visible(metadatawidget):
assert not metadatawidget.isVisible()
@bdd.then(bdd.parsers.parse("the metadata text should contain '{text}'"))
def check_text_in_metadata(metadata, text):
assert text in metadata.text()
def check_text_in_metadata(metadatawidget, text):
assert text in metadatawidget.text()
@bdd.then(bdd.parsers.parse("the metadata text should not contain '{text}'"))
def check_text_not_in_metadata(metadata, text):
assert text not in metadata.text()
def check_text_not_in_metadata(metadatawidget, text):
assert text not in metadatawidget.text()

View File

@ -11,7 +11,7 @@ Feature: Write an image to disk
| new_path.png |
| new_path.tiff |
@exif
@metadata
Scenario: Write image preserving exif information
Given I open any image
When I add exif information

View File

@ -1,6 +1,6 @@
Feature: Ensure the application works correctly without optional dependencies
@noexif
@nometadata
Scenario: No metadata command
Given I open any image
When I run metadata

View File

@ -0,0 +1,65 @@
# vim: ft=python fileencoding=utf-8 sw=4 et sts=4
# This file is part of vimiv.
# Copyright 2017-2023 Christian Karl (karlch) <karlch at protonmail dot com>
# License: GNU GPL v3, see the "LICENSE" and "AUTHORS" files for details.
"""Tests for vimiv.imutils.metadata."""
import pytest
from vimiv.imutils import metadata
from vimiv.plugins import metadata_piexif, metadata_pyexiv2
@pytest.fixture(autouse=True)
def reset_to_default(cleanup_helper):
"""Fixture to ensure everything is reset to default after testing."""
registry = list(metadata._registry)
yield
metadata._registry = registry
@pytest.fixture
def nometadata():
metadata._registry = []
@pytest.fixture
def piexif():
metadata._registry = []
metadata_piexif.init()
@pytest.fixture
def pyexiv2():
metadata._registry = []
metadata_pyexiv2.init()
@pytest.mark.parametrize(
"methodname, args",
(
("copy_metadata", ("dest.jpg",)),
("get_date_time", ()),
("get_metadata", ([],)),
("get_keys", ()),
),
)
def test_handler_raises(nometadata, methodname, args):
assert not metadata.has_metadata_support()
handler = metadata.MetadataHandler("path")
method = getattr(handler, methodname)
with pytest.raises(metadata.MetadataError):
method(*args)
def test_piexif_initializes(piexif):
assert metadata_piexif.MetadataPiexif in metadata._registry
assert metadata.has_metadata_support()
def test_pyexiv2_initializes(pyexiv2):
assert metadata_pyexiv2.MetadataPyexiv2 in metadata._registry
assert metadata.has_metadata_support()

View File

@ -189,6 +189,17 @@ def test_order_setting_sort(
assert sorted_values == expected_values
@pytest.mark.parametrize("reverse", [True, False])
@pytest.mark.parametrize("ignore_case", [True, False])
def test_order_setting_sort_none(monkeypatch, reverse, ignore_case):
values = ["a5.j", "A6.j", "a11.j", "a3.j"]
monkeypatch.setattr(settings.sort.reverse, "value", reverse)
monkeypatch.setattr(settings.sort.ignore_case, "value", ignore_case)
o = settings.OrderSetting("order", "none")
sorted_values = o.sort(values)
assert sorted_values == values
@pytest.mark.parametrize("ignore_case", [True, False])
def test_order_setting_sort_ignore_case(monkeypatch, ignore_case):
monkeypatch.setattr(settings.sort.ignore_case, "value", ignore_case)

View File

@ -1,61 +0,0 @@
# vim: ft=python fileencoding=utf-8 sw=4 et sts=4
# This file is part of vimiv.
# Copyright 2017-2023 Christian Karl (karlch) <karlch at protonmail dot com>
# License: GNU GPL v3, see the "LICENSE" and "AUTHORS" files for details.
"""Tests for vimiv.imutils.exif."""
import pytest
from vimiv.imutils import exif
@pytest.fixture(params=[exif.ExifHandler, exif._ExifHandlerPiexif])
def exif_handler(request):
"""Parametrized pytest fixture to yield the different exif handlers."""
yield request.param
def test_check_exif_dependency():
default = None
assert exif.check_exif_dependancy(default) == default
def test_check_exif_dependency_piexif(piexif):
default = None
assert exif.check_exif_dependancy(default) == exif._ExifHandlerPiexif
def test_check_exif_dependency_noexif(noexif):
default = None
assert exif.check_exif_dependancy(default) == exif._ExifHandlerBase
@pytest.mark.parametrize(
"methodname, args",
(
("copy_exif", ("dest.jpg",)),
("exif_date_time", ()),
("get_formatted_exif", ([],)),
("get_keys", ()),
),
)
def test_handler_base_raises(methodname, args):
handler = exif._ExifHandlerBase()
method = getattr(handler, methodname)
with pytest.raises(exif.UnsupportedExifOperation):
method(*args)
@pytest.mark.parametrize(
"handler, expected_msg",
(
(exif.ExifHandler, "not supported by pyexiv2"),
(exif._ExifHandlerPiexif, "not supported by piexif"),
(exif._ExifHandlerBase, "not supported. Please install"),
),
)
def test_handler_exception_customization(handler, expected_msg):
with pytest.raises(exif.UnsupportedExifOperation, match=expected_msg):
handler.raise_exception("test operation")

View File

@ -9,7 +9,6 @@
import pytest
from vimiv import version
from vimiv.imutils import exif
@pytest.fixture
@ -23,18 +22,3 @@ def test_svg_support_info():
def test_no_svg_support_info(no_svg_support):
assert "svg support: false" in version.info().lower()
@pytest.mark.pyexiv2
def test_pyexiv2_info():
assert exif.pyexiv2.__version__ in version.info()
@pytest.mark.piexif
def test_piexif_info():
assert exif.piexif.VERSION in version.info()
def test_no_exif_support_info(noexif):
assert "piexif: none" in version.info().lower()
assert "pyexiv2: none" in version.info().lower()

View File

@ -11,6 +11,7 @@ from PyQt5.QtGui import QPixmap
import pytest
from vimiv.api import settings
from vimiv.utils import thumbnail_manager
@ -24,6 +25,28 @@ def manager(qtbot, tmp_path, mocker):
yield thumbnail_manager.ThumbnailManager(None)
def test_thumbnail_save_disabled(monkeypatch, qtbot, tmp_path, manager):
monkeypatch.setattr(settings.thumbnail.save, "value", False)
no_thumbnail_path = str(tmp_path / "no_thumbnail.jpg")
QPixmap(300, 300).save(no_thumbnail_path, "jpg")
manager.create_thumbnails_async([no_thumbnail_path])
check_thumbails_created(qtbot, manager, 0)
def test_thumbnail_save_disabled_no_delete_old(monkeypatch, qtbot, tmp_path, manager):
monkeypatch.setattr(settings.thumbnail.save, "value", True)
has_thumbnail_path = str(tmp_path / "has_thumbnail.jpg")
QPixmap(300, 300).save(has_thumbnail_path, "jpg")
manager.create_thumbnails_async([has_thumbnail_path])
check_thumbails_created(qtbot, manager, 1)
monkeypatch.setattr(settings.thumbnail.save, "value", False)
no_thumbnail_path = str(tmp_path / "no_thumbnail.jpg")
QPixmap(300, 300).save(no_thumbnail_path, "jpg")
manager.create_thumbnails_async([has_thumbnail_path, no_thumbnail_path])
check_thumbails_created(qtbot, manager, 1)
@pytest.mark.parametrize("n_paths", (1, 5))
def test_create_n_thumbnails(qtbot, tmp_path, manager, n_paths):
# Create images to create thumbnails of

View File

@ -23,7 +23,6 @@ def cached_method_cls(mocker):
"""Fixture to retrieve a class with mock utilities and a cached method."""
class CachedMethodCls:
RESULT = 42
def __init__(self):
@ -143,7 +142,6 @@ def test_parameter_names(function):
@pytest.mark.parametrize("type_hint", ("int", int))
def test_slot(type_hint):
class Dummy(QObject):
signal = pyqtSignal(int)
def __init__(self):

13
tox.ini
View File

@ -10,8 +10,6 @@ basepython = {env:PYTHON:python3}
deps =
-r{toxinidir}/misc/requirements/requirements_tests.txt
pyqt: -r{toxinidir}/misc/requirements/requirements.txt
pyqt511: PyQt5==5.11.3
pyqt512: PyQt5==5.12.3
pyqt513: PyQt5==5.13.2
pyqt514: PyQt5==5.14.2
pyqt515: -r{toxinidir}/misc/requirements/requirements.txt
@ -32,7 +30,7 @@ deps =
commands =
pylint vimiv scripts/pylint_checkers
{toxinidir}/scripts/lint_tests.py tests
pycodestyle vimiv tests scripts/pylint_checkers
black --check --diff --exclude ".*syntax_error.*" vimiv tests scripts/pylint_checkers
pydocstyle vimiv scripts/pylint_checkers
allowlist_externals =
{toxinidir}/scripts/lint_tests.py
@ -80,14 +78,7 @@ deps = {[testenv:docs]deps}
commands =
{toxinidir}/scripts/src2rst.py
sphinx-build -b man docs misc
# Settings for pycodestyle
[pycodestyle]
max-line-length = 88
# E203: whitespace before ':' wrongly raised for slicing
# E501: line too long checked by pylint
# W503: line break before binary operator does not conform to pep8
ignore = E203,E501,W503
allowlist_externals = {[testenv:docs]allowlist_externals}
# Settings for check-manifest
[check-manifest]

View File

@ -203,12 +203,7 @@ def get_by_name(name: str) -> Mode:
class _ModeWidget(QWidget):
"""Helper class defining the requirements for mode widgets.
This should in principle be solved using protocols, but these are only available
starting from python 3.8 and we still support python 3.6.
See https://docs.python.org/3/library/typing.html#typing.Protocol for more details.
"""
"""Helper class defining the requirements for mode widgets."""
def current(self) -> str:
"""Return the current path valid for this mode."""

View File

@ -325,6 +325,7 @@ class OrderSetting(Setting):
"alphabetical": str,
"natural": natural_sort,
"recently-modified": os.path.getmtime,
"none": lambda x: 0,
}
STR_ORDER_TYPES = "alphabetical", "natural"
@ -469,6 +470,11 @@ class thumbnail: # pylint: disable=invalid-name
"""Namespace for thumbnail related settings."""
size = ThumbnailSizeSetting("thumbnail.size", 128, desc="Size of thumbnails")
save = BoolSetting(
"thumbnail.save",
True,
desc="Save new thumbnails to disk in the shared icon cache for later use",
)
class slideshow: # pylint: disable=invalid-name

View File

@ -35,6 +35,8 @@ class _SignalHandler(QObject):
svg_loaded: Emitted when the file handler loaded a new vector graphic.
arg1: The path as the VectorGraphic class is constructed directly.
arg2: True if it is only reloaded.
plugins_loaded: Emitted when the user plugins have been loaded.
"""
# Emitted when new images should be loaded
@ -53,6 +55,9 @@ class _SignalHandler(QObject):
movie_loaded = pyqtSignal(QMovie, bool)
svg_loaded = pyqtSignal(str, bool)
# Plugins loaded
plugins_loaded = pyqtSignal()
_signal_handler = _SignalHandler() # Instance of Qt signal handler to work with
@ -65,3 +70,4 @@ image_changed = _signal_handler.image_changed
pixmap_loaded = _signal_handler.pixmap_loaded
movie_loaded = _signal_handler.movie_loaded
svg_loaded = _signal_handler.svg_loaded
plugins_loaded = _signal_handler.plugins_loaded

View File

@ -28,8 +28,8 @@ except ImportError: # pragma: no cover # PyQt is there in tests, using None is
PYQT_VERSION = None # type: ignore
PYTHON_REQUIRED_VERSION = (3, 6)
PYQT_REQUIRED_VERSION = (5, 9, 2)
PYTHON_REQUIRED_VERSION = (3, 8)
PYQT_REQUIRED_VERSION = (5, 13, 2)
ERR_CODE = 2

View File

@ -23,11 +23,15 @@ from vimiv import api
def set_command(name: str, value: List[str]):
"""Set an option.
**syntax:** ``:set name [value]``
**syntax:** ``:set name[!] [[+|-]value]``
positional arguments:
* ``name``: Name of the setting to set.
* ``value``: Value to set the setting to. If not given, set to default.
Append a ``!`` to toggle the value of boolean settings.
* ``value``: Value to set the setting to.
Prepend with ``+`` / ``-`` to increment/decrement the value of numerical
settings.
If not given, set to default.
"""
strvalue = " ".join(value) # List comes from nargs='*'
try:

View File

@ -470,10 +470,7 @@ class LibraryModel(QStandardItemModel):
"""
get_size = files.get_size_directory if are_directories else files.get_size_file
mark_prefix = api.mark.indicator + " "
# See https://github.com/PyCQA/pylint/issues/7963
# TODO remove once newer pylint version with fix was released
enum_start = self.rowCount() + 1
for i, path in enumerate(paths, start=enum_start):
for i, path in enumerate(paths, start=self.rowCount() + 1):
name = os.path.basename(path)
if are_directories:
name = utils.add_html(name + "/", "b")

View File

@ -20,7 +20,6 @@ from vimiv.gui.keyhintwidget import KeyhintWidget
from vimiv.gui.library import Library
from vimiv.gui.thumbnail import ThumbnailView
from vimiv.gui.message import Message
from vimiv.gui.metadatawidget import MetadataWidget
from vimiv.gui.statusbar import StatusBar
@ -50,13 +49,12 @@ class MainWindow(QWidget):
self._overlays.append(KeyhintWidget(self))
self._overlays.append(Message(self))
self._overlays.append(CommandWidget(self))
if MetadataWidget is not None: # Not defined if there is no exif support
self._overlays.append(MetadataWidget(self))
# Connect signals
api.status.signals.update.connect(self._set_title)
api.settings.statusbar.show.changed.connect(self._update_overlay_geometry)
api.modes.MANIPULATE.first_entered.connect(self._init_manipulate)
api.prompt.question_asked.connect(self._run_prompt)
api.signals.plugins_loaded.connect(self._init_metadata)
@utils.slot
def _init_manipulate(self):
@ -66,6 +64,16 @@ class MainWindow(QWidget):
manipulate_widget = Manipulate(self)
self.add_overlay(manipulate_widget)
@utils.slot
def _init_metadata(self):
"""Initialize metadata widget in case we have metadata support."""
from vimiv.imutils import metadata
if metadata.has_metadata_support():
from vimiv.gui.metadatawidget import MetadataWidget
self._overlays.append(MetadataWidget(self))
@api.keybindings.register("f", "fullscreen", mode=api.modes.MANIPULATE)
@api.keybindings.register("f", "fullscreen")
@api.commands.register(mode=api.modes.MANIPULATE)

View File

@ -13,169 +13,169 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel, QSizePolicy, QWidget
from vimiv import api, utils
from vimiv.imutils import exif
from vimiv.imutils import metadata
from vimiv.config import styles
_logger = utils.log.module_logger(__name__)
if exif.has_exif_support:
class MetadataWidget(QLabel):
"""Overlay widget to display image metadata.
class MetadataWidget(QLabel):
"""Overlay widget to display image metadata.
The display of the widget can be toggled by command. It is filled with exif
metadata information of the current image.
The display of the widget can be toggled by command. It is filled with
metadata information of the current image.
Attributes:
_mainwindow_bottom: y-coordinate of the bottom of the mainwindow.
_mainwindow_width: width of the mainwindow.
_path: Absolute path of the current image to load exif metadata of.
_current_set: Holds a string of the currently selected keyset.
_handler: ExifHandler for _path or None. Use the handler property to access.
Attributes:
_mainwindow_bottom: y-coordinate of the bottom of the mainwindow.
_mainwindow_width: width of the mainwindow.
_path: Absolute path of the current image to load metadata of.
_current_set: Holds a string of the currently selected keyset.
_handler: MetadataHandler for _path or None. Use its property for access.
"""
STYLESHEET = """
QLabel {
font: {statusbar.font};
color: {statusbar.fg};
background: {metadata.bg};
padding: {metadata.padding};
border-top-left-radius: {metadata.border_radius};
}
"""
@api.objreg.register
def __init__(self, parent: QWidget):
super().__init__(parent=parent)
styles.apply(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.setTextFormat(Qt.RichText)
self._mainwindow_bottom = 0
self._mainwindow_width = 0
self._path = ""
self._current_set = ""
self._handler: Optional[metadata.MetadataHandler] = None
api.signals.new_image_opened.connect(self._on_image_opened)
api.settings.metadata.current_keyset.changed.connect(self._update_text)
self.hide()
@property
def handler(self) -> metadata.MetadataHandler:
"""Return the MetadataHandler for the current path."""
if self._handler is None:
self._handler = metadata.MetadataHandler(self._path)
return self._handler
@api.keybindings.register("i", "metadata", mode=api.modes.IMAGE)
@api.commands.register(mode=api.modes.IMAGE)
def metadata(self, count: Optional[int] = None):
"""Toggle display of metadata of current image.
**count:** Select the key set to display instead.
.. hint::
5 default key sets are provided and mapped to the counts 1-5. To
override them or add your own, extend the METADATA section in your
configfile like this::
keys2 = Override,Second,Set
keys4 = New,Fourth,Set
where the values must be a comma-separated list of valid metadata keys.
"""
STYLESHEET = """
QLabel {
font: {statusbar.font};
color: {statusbar.fg};
background: {metadata.bg};
padding: {metadata.padding};
border-top-left-radius: {metadata.border_radius};
}
"""
@api.objreg.register
def __init__(self, parent: QWidget):
super().__init__(parent=parent)
styles.apply(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.setTextFormat(Qt.RichText)
self._mainwindow_bottom = 0
self._mainwindow_width = 0
self._path = ""
self._current_set = ""
self._handler: Optional[exif.ExifHandler] = None
api.signals.new_image_opened.connect(self._on_image_opened)
api.settings.metadata.current_keyset.changed.connect(self._update_text)
if count is not None:
try:
_logger.debug("Switch keyset")
new_keyset = api.settings.metadata.keysets[count]
api.settings.metadata.current_keyset.value = new_keyset
if not self.isVisible():
_logger.debug("Showing widget")
self.raise_()
self.show()
except KeyError:
raise api.commands.CommandError(f"Invalid key set option {count}")
elif self.isVisible():
_logger.debug("Hiding widget")
self.hide()
else:
_logger.debug("Showing widget")
self._update_text()
self.raise_()
self.show()
@property
def handler(self) -> exif.ExifHandler:
"""Return the ExifHandler for the current path."""
if self._handler is None:
self._handler = exif.ExifHandler(self._path)
return self._handler
@api.commands.register(mode=api.modes.IMAGE)
def metadata_list_keys(self, n_cols: int = 3, to_term: bool = False):
"""Display a list of all valid metadata keys for the current image.
@api.keybindings.register("i", "metadata", mode=api.modes.IMAGE)
@api.commands.register(mode=api.modes.IMAGE)
def metadata(self, count: Optional[int] = None):
"""Toggle display of exif metadata of current image.
**syntax:** ``:metadata-list-keys [--n-cols=NUMBER] [--to-term]``
**count:** Select the key set to display instead.
optional arguments:
* ``--n-cols``: Number of columns used to display the keys.
* ``--to-term``: Print the keys to the terminal instead.
"""
.. hint::
5 default key sets are provided and mapped to the counts 1-5. To
override them or add your own, extend the METADATA section in your
configfile like this::
keys2 = Override,Second,Set
keys4 = New,Fourth,Set
where the values must be a comma-separated list of valid metadata keys.
"""
if count is not None:
try:
_logger.debug("Switch keyset")
new_keyset = api.settings.metadata.keysets[count]
api.settings.metadata.current_keyset.value = new_keyset
if not self.isVisible():
_logger.debug("Showing widget")
self.raise_()
self.show()
except KeyError:
raise api.commands.CommandError(f"Invalid key set option {count}")
elif self.isVisible():
_logger.debug("Hiding widget")
self.hide()
else:
_logger.debug("Showing widget")
self._update_text()
self.raise_()
self.show()
@api.commands.register(mode=api.modes.IMAGE)
def metadata_list_keys(self, n_cols: int = 3, to_term: bool = False):
"""Display a list of all valid metadata keys for the current image.
**syntax:** ``:metadata-list-keys [--n-cols=NUMBER] [--to-term]``
optional arguments:
* ``--n-cols``: Number of columns used to display the keys.
* ``--to-term``: Print the keys to the terminal instead.
"""
keys = sorted(set(self.handler.get_keys()))
if to_term:
print(*keys, sep="\n")
elif n_cols < 1:
raise api.commands.CommandError("Number of columns must be positive")
else:
columns = list(utils.split(keys, n_cols))
table = utils.format_html_table(
itertools.zip_longest(*columns, fillvalue="")
)
self.setText(table)
self._update_geometry()
self.show()
def update_geometry(self, window_width, window_bottom):
"""Adapt location when main window geometry changes."""
self._mainwindow_width = window_width
self._mainwindow_bottom = window_bottom
keys = sorted(set(self.handler.get_keys()))
if to_term:
print(*keys, sep="\n")
elif n_cols < 1:
raise api.commands.CommandError("Number of columns must be positive")
else:
columns = list(utils.split(keys, n_cols))
table = utils.format_html_table(
itertools.zip_longest(*columns, fillvalue="")
)
self.setText(table)
self._update_geometry()
self.show()
def _update_geometry(self):
"""Update geometry according to current text content and window location."""
self.adjustSize()
y = self._mainwindow_bottom - self.height()
self.setGeometry(
self._mainwindow_width - self.width(), y, self.width(), self.height()
)
def update_geometry(self, window_width, window_bottom):
"""Adapt location when main window geometry changes."""
self._mainwindow_width = window_width
self._mainwindow_bottom = window_bottom
self._update_geometry()
def _update_text(self):
"""Update the metadata text if the current image has not been loaded."""
if self._current_set == api.settings.metadata.current_keyset.value:
return
_logger.debug(
"%s: reading exif of %s", self.__class__.__qualname__, self._path
)
keys = [
e.strip() for e in api.settings.metadata.current_keyset.value.split(",")
]
_logger.debug(f"Read metadata.current_keys {keys}")
formatted_exif = self.handler.get_formatted_exif(keys)
if formatted_exif:
self.setText(utils.format_html_table(formatted_exif.values()))
def _update_geometry(self):
"""Update geometry according to current text content and window location."""
self.adjustSize()
y = self._mainwindow_bottom - self.height()
self.setGeometry(
self._mainwindow_width - self.width(), y, self.width(), self.height()
)
def _update_text(self):
"""Update the metadata text if the current image has not been loaded."""
if self._current_set == api.settings.metadata.current_keyset.value:
return
_logger.debug(
"%s: reading metadata of %s", self.__class__.__qualname__, self._path
)
keys = [
e.strip() for e in api.settings.metadata.current_keyset.value.split(",")
]
_logger.debug(f"Extracting metadata for keys: {keys}")
try:
data = self.handler.get_metadata(keys)
if data:
# Sort data according to order provided in config
sorted_data = [data[key] for key in keys if key in data]
self.setText(utils.format_html_table(sorted_data))
else:
self.setText("No matching metadata found")
self._update_geometry()
self._current_set = api.settings.metadata.current_keyset.value
except metadata.MetadataError as e:
self.setText(str(e))
self._update_geometry()
self._current_set = api.settings.metadata.current_keyset.value
@utils.slot
def _on_image_opened(self, path: str):
"""Load new image and update text if the widget is currently visible."""
self._path = path
self._current_set = ""
self._handler = None
if self.isVisible():
self._update_text()
else:
MetadataWidget = None # type: ignore
@utils.slot
def _on_image_opened(self, path: str):
"""Load new image and update text if the widget is currently visible."""
self._path = path
self._current_set = ""
self._handler = None
if self.isVisible():
self._update_text()

View File

@ -38,7 +38,7 @@ The image widget in ``vimiv.gui.image`` connects to these signals and displays
the appropriate Qt widget.
"""
from vimiv.imutils import exif
from vimiv.imutils import metadata
from vimiv.imutils.edit_handler import EditHandler
from vimiv.imutils.filelist import current, pathlist
from vimiv.imutils.filelist import SignalHandler as _FilelistSignalHandler

View File

@ -172,7 +172,7 @@ def write_pixmap(pixmap, path, original_path):
Args:
pixmap: The QPixmap to write.
path: Path to write the pixmap to.
original_path: Original path of the opened pixmap to retrieve exif information.
original_path: Original path of the opened pixmap to retrieve metadata.
"""
try:
_can_write(pixmap, path)
@ -210,10 +210,10 @@ def _write(pixmap, path, original_path):
handle, filename = tempfile.mkstemp(suffix=ext)
os.close(handle)
pixmap.save(filename)
# Copy exif info from original file to new file
# Best-effort copy metadata info from original file to new file
try:
imutils.exif.ExifHandler(original_path).copy_exif(filename)
except imutils.exif.UnsupportedExifOperation:
imutils.metadata.MetadataHandler(original_path).copy_metadata(filename)
except imutils.metadata.MetadataError:
pass
shutil.move(filename, path)
# Check if valid image was created

View File

@ -1,304 +0,0 @@
# vim: ft=python fileencoding=utf-8 sw=4 et sts=4
# This file is part of vimiv.
# Copyright 2017-2023 Christian Karl (karlch) <karlch at protonmail dot com>
# License: GNU GPL v3, see the "LICENSE" and "AUTHORS" files for details.
"""Utility functions and classes for exif handling.
All exif related tasks are implemented in this module. The heavy lifting is done using
one of the supported exif libraries, i.e.
* piexif (https://pypi.org/project/piexif/) and
* pyexiv2 (https://pypi.org/project/py3exiv2/).
"""
import contextlib
import itertools
from typing import Any, Dict, Tuple, NoReturn, Sequence, Iterable
from vimiv.utils import log, lazy, is_hex
pyexiv2 = lazy.import_module("pyexiv2", optional=True)
piexif = lazy.import_module("piexif", optional=True)
_logger = log.module_logger(__name__)
ExifDictT = Dict[Any, Tuple[str, str]]
class UnsupportedExifOperation(NotImplementedError):
"""Raised if an exif operation is not supported by the used library if any."""
class _ExifHandlerBase:
"""Handler to load and copy exif information of a single image.
This class provides the interface for handling exif support. By default none of the
operations are implemented. Instead it is up to a child class which wraps around one
of the supported exif libraries to implement the methods it can.
"""
MESSAGE_SUFFIX = ". Please install pyexiv2 or piexif for exif support."
def __init__(self, _filename=""):
pass
def copy_exif(self, _dest: str, _reset_orientation: bool = True) -> None:
"""Copy exif information from current image to dest.
Args:
dest: Path to write the exif information to.
reset_orientation: If true, reset the exif orientation tag to normal.
"""
self.raise_exception("Copying exif data")
def exif_date_time(self) -> str:
"""Get exif creation date and time as formatted string."""
self.raise_exception("Retrieving exif date-time")
def get_formatted_exif(self, _desired_keys: Sequence[str]) -> ExifDictT:
"""Get a dictionary of formatted exif values."""
self.raise_exception("Getting formatted exif data")
def get_keys(self) -> Iterable[str]:
"""Retrieve the name of all exif keys available."""
self.raise_exception("Getting exif keys")
@classmethod
def raise_exception(cls, operation: str) -> NoReturn:
"""Raise an exception for a not implemented exif operation."""
msg = f"{operation} is not supported{cls.MESSAGE_SUFFIX}"
_logger.warning(msg, once=True)
raise UnsupportedExifOperation(msg)
class _ExifHandlerPiexif(_ExifHandlerBase):
"""Implementation of ExifHandler based on piexif."""
MESSAGE_SUFFIX = " by piexif."
def __init__(self, filename=""):
super().__init__(filename)
try:
self._metadata = piexif.load(filename)
except FileNotFoundError:
_logger.debug("File %s not found", filename)
self._metadata = None
except piexif.InvalidImageDataError:
log.warning(
"Piexif only supports the file types JPEG and TIFF.<br>\n"
"Please install pyexiv2 for better file type support.<br>\n"
"For more information see<br>\n"
"https://karlch.github.io/vimiv-qt/documentation/exif.html",
once=True,
)
self._metadata = None
def get_formatted_exif(self, desired_keys: Sequence[str]) -> ExifDictT:
desired_keys = [key.rpartition(".")[2] for key in desired_keys]
exif = {}
if self._metadata is None:
return {}
try:
for ifd in self._metadata:
if ifd == "thumbnail":
continue
for tag in self._metadata[ifd]:
keyname = piexif.TAGS[ifd][tag]["name"]
keytype = piexif.TAGS[ifd][tag]["type"]
val = self._metadata[ifd][tag]
_logger.debug(
f"name: {keyname}\
type: {keytype}\
value: {val}\
tag: {tag}"
)
if keyname not in desired_keys:
_logger.debug(f"Ignoring key {keyname}")
continue
if keytype in (
piexif.TYPES.Byte,
piexif.TYPES.Short,
piexif.TYPES.Long,
piexif.TYPES.SByte,
piexif.TYPES.SShort,
piexif.TYPES.SLong,
piexif.TYPES.Float,
piexif.TYPES.DFloat,
): # integer and float
exif[keyname] = (keyname, str(val))
elif keytype in (
piexif.TYPES.Ascii,
piexif.TYPES.Undefined,
): # byte encoded
exif[keyname] = (keyname, val.decode())
elif keytype in (
piexif.TYPES.Rational,
piexif.TYPES.SRational,
): # (int, int) <=> numerator, denominator
exif[keyname] = (keyname, f"{val[0]}/{val[1]}")
except KeyError:
return {}
return exif
def get_keys(self) -> Iterable[str]:
return (
piexif.TAGS[ifd][tag]["name"]
for ifd in self._metadata
if ifd != "thumbnail"
for tag in self._metadata[ifd]
)
def copy_exif(self, dest: str, reset_orientation: bool = True) -> None:
if self._metadata is None:
return
try:
if reset_orientation:
with contextlib.suppress(KeyError):
self._metadata["0th"][
piexif.ImageIFD.Orientation
] = ExifOrientation.Normal
exif_bytes = piexif.dump(self._metadata)
piexif.insert(exif_bytes, dest)
_logger.debug("Successfully wrote exif data for '%s'", dest)
except ValueError:
_logger.debug("No exif data in '%s'", dest)
def exif_date_time(self) -> str:
if self._metadata is None:
return ""
with contextlib.suppress(KeyError):
return self._metadata["0th"][piexif.ImageIFD.DateTime].decode()
return ""
def check_exif_dependancy(handler):
"""Decorator for ExifHandler which requires the optional pyexiv2 module.
If pyexiv2 is available, the class is left as it is. If pyexiv2 is not available
but the less powerful piexif module is, _ExifHandlerPiexif is returned instead.
If none of the two modules are available, the base implementation which always
throws an exception is returned.
Args:
handler: The class to be decorated.
"""
if pyexiv2:
return handler
if piexif:
return _ExifHandlerPiexif
_logger.warning(
"There is no exif support and therefore:\n"
"1. Exif data is lost when writing images to disk.\n"
"2. The `:metadata` command and associated `i` keybinding is not available.\n"
"3. The {exif-date-time} statusbar module is not available.\n\n"
"Please install pyexiv2 or piexif to silence this warning.\n"
"For more information see\n"
"https://karlch.github.io/vimiv-qt/documentation/exif.html\n"
)
return _ExifHandlerBase
@check_exif_dependancy
class ExifHandler(_ExifHandlerBase):
"""Main ExifHandler implementation based on pyexiv2."""
MESSAGE_SUFFIX = " by pyexiv2."
def __init__(self, filename=""):
super().__init__(filename)
try:
self._metadata = pyexiv2.ImageMetadata(filename)
self._metadata.read()
except FileNotFoundError:
_logger.debug("File %s not found", filename)
def get_formatted_exif(self, desired_keys: Sequence[str]) -> ExifDictT:
exif = {}
for base_key in desired_keys:
# For backwards compability, assume it has one of the following prefixes
for prefix in ["", "Exif.Image.", "Exif.Photo."]:
key = f"{prefix}{base_key}"
try:
key_name = self._metadata[key].name
try:
key_value = self._metadata[key].human_value
# Not all metadata (i.e. IPTC) provide human_value, take raw_value
except AttributeError:
value = self._metadata[key].raw_value
# For IPTC the raw_value is a list of strings
if isinstance(value, list):
key_value = ", ".join(value)
else:
key_value = value
exif[key] = (key_name, key_value)
break
except KeyError:
_logger.debug("Key %s is invalid for the current image", key)
return exif
def get_keys(self) -> Iterable[str]:
return (key for key in self._metadata if not is_hex(key.rpartition(".")[2]))
def copy_exif(self, dest: str, reset_orientation: bool = True) -> None:
if reset_orientation:
with contextlib.suppress(KeyError):
self._metadata["Exif.Image.Orientation"] = ExifOrientation.Normal
try:
dest_image = pyexiv2.ImageMetadata(dest)
dest_image.read()
# File types restrict the metadata type they can store.
# Try copying all types one by one and skip if it fails.
for copy_args in set(itertools.permutations((True, False, False, False))):
with contextlib.suppress(ValueError):
self._metadata.copy(dest_image, *copy_args)
dest_image.write()
_logger.debug("Successfully wrote exif data for '%s'", dest)
except FileNotFoundError:
_logger.debug("Failed to write exif data. Destination '%s' not found", dest)
except OSError as e:
_logger.debug("Failed to write exif data for '%s': '%s'", dest, str(e))
def exif_date_time(self) -> str:
with contextlib.suppress(KeyError):
return self._metadata["Exif.Image.DateTime"].raw_value
return ""
has_exif_support = ExifHandler != _ExifHandlerBase
class ExifOrientation:
"""Namespace for exif orientation tags.
For more information see: http://jpegclub.org/exif_orientation.html.
"""
Unspecified = 0
Normal = 1
HorizontalFlip = 2
Rotation180 = 3
VerticalFlip = 4
Rotation90HorizontalFlip = 5
Rotation90 = 6
Rotation90VerticalFlip = 7
Rotation270 = 8

View File

@ -141,8 +141,8 @@ def exif_date_time() -> str:
be used as basis to work with.
"""
try:
return imutils.exif.ExifHandler(current()).exif_date_time()
except imutils.exif.UnsupportedExifOperation:
return imutils.metadata.MetadataHandler(current()).get_date_time()
except imutils.metadata.MetadataError:
return ""

View File

@ -188,6 +188,8 @@ class Transform(QTransform):
@property
def changed(self):
"""True if transformations have been applied."""
if self.current.rect().isNull():
return False
transformed = not self.isIdentity()
if self._original is None:
return transformed

281
vimiv/imutils/metadata.py Normal file
View File

@ -0,0 +1,281 @@
# vim: ft=python fileencoding=utf-8 sw=4 et sts=4
# This file is part of vimiv.
# Copyright 2017-2023 Christian Karl (karlch) <karlch at protonmail dot com>
# License: GNU GPL v3, see the "LICENSE" and "AUTHORS" files for details.
"""Utility functions and classes for metadata handling.
This module provides a common interface for all metadata related functionalities.
`MetadataHandler` is used to interact with the metadata of the current image. It
relies on the functionality provided by optionally loaded metadata plugins. Such
plugins implements the `MetadataPlugin` abstract class and registers that class
using the `register` function.
Module Attributes:
_registry: List of registered `MetadataPlugin` implementations.
"""
import abc
import contextlib
import itertools
from typing import Dict, Tuple, NoReturn, Sequence, Iterable, Type, List
from vimiv.utils import log
_logger = log.module_logger(__name__)
# Type returned by `MetadataHandler.get_metadata`.
# Key is the metadata key. Value is a tuple of descriptive name and value for that key.
MetadataDictT = Dict[str, Tuple[str, str]]
class MetadataPlugin(abc.ABC):
"""Abstract class implemented by plugins to provide metadata capabilities.
Implementations of this class are required to overwrite `__init__`, `name`,
`version`, `get_metadata` and `get_keys`.
The implementation of `copy_metadata` and `get_date_time` is optional.
"""
@abc.abstractmethod
def __init__(self, _path: str) -> None:
"""Initialize metadata handler for a specific image.
Args:
_path: Path to current image.
"""
@staticmethod
@abc.abstractmethod
def name() -> str:
"""Get the name of the used backend.
If no backend is used, return the name of the plugin.
"""
@staticmethod
@abc.abstractmethod
def version() -> str:
"""Get the version of the used backend.
If no backend is used, return an empty string.
"""
@abc.abstractmethod
def get_metadata(self, _keys: Sequence[str]) -> MetadataDictT:
"""Get value of all desired keys for the current image.
If no value is found for a certain key, do not include the key in the output.
Args:
_keys: Keys of metadata to query the image for.
Returns:
Dictionary with retrieved metadata.
"""
@abc.abstractmethod
def get_keys(self) -> Iterable[str]:
"""Get the keys for all metadata values available for the current image."""
def copy_metadata(self, _dest: str, _reset_orientation: bool = True) -> bool:
"""Copy metadata from the current image to dest image.
Args:
_dest: Path to write the metadata to.
_reset_orientation: If true, reset the exif orientation tag to normal.
Returns:
Flag indicating if copy was successful.
"""
raise NotImplementedError
def get_date_time(self) -> str:
"""Get creation date and time of the current image as formatted string."""
raise NotImplementedError
# Stores all registered metadata implementations.
_registry: List[Type[MetadataPlugin]] = []
def has_metadata_support() -> bool:
"""Indicate if `MetadataHandler` has `get_metadata()` and `get_keys()` capabilities.
Returns:
True if at least one metadata plugins has been registered.
"""
return bool(_registry)
class MetadataHandler:
"""Handle metadata related functionalities of images.
Attributes:
_path: Path to current image.
"""
def __init__(self, path: str):
self._path = path
@property
def has_copy_metadata(self) -> bool:
"""True if `MetadataHandler` has an implementation for `copy_metadata`."""
return any(e.copy_metadata != MetadataPlugin.copy_metadata for e in _registry)
@property
def has_get_date_time(self) -> bool:
"""True if `MetadataHandler` has an implementation for `get_date_time`."""
return any(e.get_date_time != MetadataPlugin.get_date_time for e in _registry)
def get_metadata(self, keys: Sequence[str]) -> MetadataDictT:
"""Get value of all desired keys from the current image.
Use all registered metadata implementations to extract the metadata from the
current image. The output of all methods is combined.
Args:
keys: Keys of metadata to query the image for.
Returns:
Dictionary with retrieved metadata.
Raises:
MetadataError
"""
if not has_metadata_support():
MetadataHandler.raise_exception("get_metadata")
out: MetadataDictT = {}
for backend in _registry:
# TODO: from 3.9 on use: c = a | b
out = {**backend(self._path).get_metadata(keys), **out}
return out
def get_keys(self) -> Iterable[str]:
"""Get the keys for all metadata values available for the current image.
Uses all registered metadata implementations to extract the available keys for
the current image. The output of all methods is combined.
Raises:
MetadataError
"""
if not has_metadata_support():
MetadataHandler.raise_exception("get_keys")
out: Iterable[str] = iter([])
for backend in _registry:
out = itertools.chain(out, backend(self._path).get_keys())
return out
def copy_metadata(self, dest: str, reset_orientation: bool = True) -> None:
"""Copy metadata from current image to dest.
Uses all registered metadata implementations that support this operation.
Args:
dest: Path to write the metadata to.
reset_orientation: If true, reset the exif orientation tag to normal.
Raises:
MetadataError
"""
if not has_metadata_support() or not self.has_copy_metadata:
MetadataHandler.raise_exception("copy_metadata")
failed = []
for backend in _registry:
with contextlib.suppress(NotImplementedError):
be = backend(self._path)
if not be.copy_metadata(dest, reset_orientation):
failed.append(be.name())
if failed:
_logger.warning(
f"The following plugins failed to copy metadata: "
f"{', '.join(failed)}<br>\n"
f"Some metadata may be missing in the destination image {dest}."
)
def get_date_time(self) -> str:
"""Get creation date and time as formatted string.
Uses the first registered metadata implementations that supports this operation.
Raises:
MetadataError
"""
if not has_metadata_support() or not self.has_get_date_time:
MetadataHandler.raise_exception("get_date_time")
for backend in _registry:
with contextlib.suppress(NotImplementedError):
out = backend(self._path).get_date_time()
# If we get an empty string, continue. We may get something better.
if out:
return out
return ""
@staticmethod
def raise_exception(operation: str) -> NoReturn:
"""Raise an exception if there is insufficient support for an operation."""
msg = f"Running {operation} is not possible. Insufficient metadata support"
_logger.warning(msg, once=True)
raise MetadataError(msg)
class MetadataError(RuntimeError):
"""Raised if for a function there is insufficient metadata support."""
def register(plugin: Type[MetadataPlugin]) -> None:
"""Register metadata plugin implementation.
All registered metadata plugin implementations are available to the
`MetadataHandler`.
Args:
plugin: Implementation of `MetadataPlugin`.
"""
_logger.debug(f"Registring metadata plugin implementation {plugin.name()}")
if plugin in _registry:
_logger.warning(
f"Metadata plugin {plugin.name()} has already been registered. Ignoring it."
)
return
_registry.append(plugin)
def get_registrations() -> List[Tuple[str, str]]:
"""List of all registered metadata plugin implementations.
Returns:
List of tuples of the form (name of backend, version of backend).
"""
return [(e.name(), e.version()) for e in _registry]
class ExifOrientation:
"""Namespace for exif orientation tags.
For more information see: http://jpegclub.org/exif_orientation.html.
"""
Unspecified = 0
Normal = 1
HorizontalFlip = 2
Rotation180 = 3
VerticalFlip = 4
Rotation90HorizontalFlip = 5
Rotation90 = 6
Rotation90VerticalFlip = 7
Rotation270 = 8

View File

@ -69,12 +69,14 @@ import types
from typing import Dict, List
from vimiv.utils import xdg, log, quotedjoin
from vimiv import api
_app_plugin_directory = os.path.dirname(__file__)
_user_plugin_directory = xdg.vimiv_data_dir("plugins")
_plugins: Dict[str, str] = {
"print": "default"
"print": "default",
"metadata": "default",
} # key: name, value: additional information
_loaded_plugins: Dict[str, types.ModuleType] = {} # key:name, value: loaded module
_logger = log.module_logger(__name__)
@ -112,6 +114,7 @@ def load() -> None:
_user_plugin_directory,
)
_logger.debug("Plugin loading completed")
api.signals.plugins_loaded.emit()
def cleanup() -> None:
@ -132,10 +135,18 @@ def cleanup() -> None:
def add_plugins(**plugins: str) -> None:
"""Add plugins to the dictionary of plugins.
Note that the plugins are prepended, and take precedence above the existing plugins.
Args:
plugins: Dictionary of plugin names with metadata to add to plugins.
"""
_plugins.update(plugins)
global _plugins
for plugin, info in _plugins.items():
if plugin not in plugins:
plugins[plugin] = info
_plugins = plugins
def get_plugins() -> Dict[str, str]:

View File

@ -6,8 +6,8 @@
"""Plugin enabling support for additional image formats.
Adds support for image formats what are not natively or by qtimageformats supported, but
though some other QT module.
Adds support for image formats that are not supported by Qt natively or by the
qtimageformats add-on, but instead require some other Qt module.
The required Qt module is not installed by Vimiv and requires explicit installation.

40
vimiv/plugins/metadata.py Normal file
View File

@ -0,0 +1,40 @@
# vim: ft=python fileencoding=utf-8 sw=4 et sts=4
# This file is part of vimiv.
# Copyright 2017-2023 Christian Karl (karlch) <karlch at protonmail dot com>
# License: GNU GPL v3, see the "LICENSE" and "AUTHORS" files for details.
"""Metadata plugin wrapping the available backends to only load one."""
from typing import Any
from vimiv.plugins import metadata_piexif, metadata_pyexiv2
from vimiv.utils import log
from vimiv.imutils import metadata
_logger = log.module_logger(__name__)
def init(info: str, *_args: Any, **_kwargs: Any) -> None:
"""Initialize metadata plugin depending on available backend.
If any other backend has already been registered, do not register any new one.
"""
if metadata.has_metadata_support():
_logger.debug(
"Not loading a default metadata backend, as one has been loaded manually"
)
elif info.lower() == "none":
_logger.debug("Not auto-loading metadata support as per user-request")
elif metadata_pyexiv2.pyexiv2 is not None:
_logger.debug("Auto-loading pyexiv2 metadata plugin")
metadata_pyexiv2.init()
elif metadata_piexif.piexif is not None:
_logger.debug("Auto-loading piexif metadata plugin")
metadata_piexif.init()
else:
_logger.warning(
"Please install either py3exiv2 or piexif for metadata support.<br>\n"
"For more information see<br>\n"
"https://karlch.github.io/vimiv-qt/documentation/metadata.html",
)

View File

@ -0,0 +1,155 @@
# vim: ft=python fileencoding=utf-8 sw=4 et sts=4
# This file is part of vimiv.
# Copyright 2017-2023 Christian Karl (karlch) <karlch at protonmail dot com>
# License: GNU GPL v3, see the "LICENSE" and "AUTHORS" files for details.
"""Metadata plugin based on piexif (https://pypi.org/project/piexif/) backend.
Properties:
- Simple and easy to install.
- Limited image type support (JPG and TIFF only).
- No formatting of metadata.
- Can only handle Exif.
"""
import contextlib
from typing import Any, Sequence, Iterable
from vimiv.imutils import metadata
from vimiv.utils import log, lazy
piexif = lazy.import_module("piexif", optional=True)
_logger = log.module_logger(__name__)
class MetadataPiexif(metadata.MetadataPlugin):
"""Provided metadata support based on piexif.
Implements `get_metadata`, `get_keys`, `copy_metadata`, and `get_date_time`.
"""
def __init__(self, path: str) -> None:
self._path = path
try:
self._metadata = piexif.load(path)
except FileNotFoundError:
_logger.debug("File %s not found", path)
self._metadata = None
except piexif.InvalidImageDataError:
log.warning(
"Piexif only supports the file types JPEG and TIFF.<br>\n"
"Please use another metadata plugin for better file type support.<br>\n"
"For more information see<br>\n"
"https://karlch.github.io/vimiv-qt/documentation/metadata.html",
once=True,
)
self._metadata = None
@staticmethod
def name() -> str:
"""Get the name of the used backend."""
return "piexif"
@staticmethod
def version() -> str:
"""Get the version of the used backend."""
return piexif.VERSION
def get_metadata(self, desired_keys: Sequence[str]) -> metadata.MetadataDictT:
"""Get value of all desired keys for the current image."""
out = {}
# The keys in the default config are of the form `group.subgroup.key`. However,
# piexif only uses `key` for the indexing. Strip `group.subgroup` prefix for the
# metadata extraction, but maintain the long key in the returned dict.
desired_keys_map = {key.rpartition(".")[2]: key for key in desired_keys}
if self._metadata is None:
return {}
try:
for ifd in self._metadata:
if ifd == "thumbnail":
continue
for tag in self._metadata[ifd]:
keyname = piexif.TAGS[ifd][tag]["name"]
keytype = piexif.TAGS[ifd][tag]["type"]
val = self._metadata[ifd][tag]
if keyname not in desired_keys_map:
continue
if keytype in (
piexif.TYPES.Byte,
piexif.TYPES.Short,
piexif.TYPES.Long,
piexif.TYPES.SByte,
piexif.TYPES.SShort,
piexif.TYPES.SLong,
piexif.TYPES.Float,
piexif.TYPES.DFloat,
): # integer and float
out[desired_keys_map[keyname]] = (keyname, str(val))
elif keytype in (
piexif.TYPES.Ascii,
piexif.TYPES.Undefined,
): # byte encoded
out[desired_keys_map[keyname]] = (keyname, val.decode())
elif keytype in (
piexif.TYPES.Rational,
piexif.TYPES.SRational,
): # (int, int) <=> numerator, denominator
out[desired_keys_map[keyname]] = (keyname, f"{val[0]}/{val[1]}")
except KeyError:
return {}
return out
def get_keys(self) -> Iterable[str]:
"""Get the keys for all metadata values available for the current image."""
if self._metadata is None:
return iter([])
return (
piexif.TAGS[ifd][tag]["name"]
for ifd in self._metadata
if ifd != "thumbnail"
for tag in self._metadata[ifd]
)
def copy_metadata(self, dest: str, reset_orientation: bool = True) -> bool:
"""Copy metadata from the current image to dest image."""
if self._metadata is None:
return False
try:
if reset_orientation:
with contextlib.suppress(KeyError):
self._metadata["0th"][
piexif.ImageIFD.Orientation
] = metadata.ExifOrientation.Normal
exif_bytes = piexif.dump(self._metadata)
piexif.insert(exif_bytes, dest)
return True
except ValueError:
return False
def get_date_time(self) -> str:
"""Get creation date and time of the current image as formatted string."""
if self._metadata is None:
return ""
with contextlib.suppress(KeyError):
return self._metadata["0th"][piexif.ImageIFD.DateTime].decode()
return ""
def init(*_args: Any, **_kwargs: Any) -> None:
"""Initialize piexif handler if piexif is available."""
if piexif is not None:
metadata.register(MetadataPiexif)
else:
_logger.warning("Please install piexif to use this plugin")

View File

@ -0,0 +1,133 @@
# vim: ft=python fileencoding=utf-8 sw=4 et sts=4
# This file is part of vimiv.
# Copyright 2017-2023 Christian Karl (karlch) <karlch at protonmail dot com>
# License: GNU GPL v3, see the "LICENSE" and "AUTHORS" files for details.
"""Metadata plugin based on pyexiv2 (https://pypi.org/project/py3exiv2/) backend.
Properties:
- Shared libraries as dependencies.
- Formatted Metadata.
- Reads Exif, IPTC and XMP.
"""
import contextlib
import itertools
from typing import Any, Sequence, Iterable
from vimiv.imutils import metadata
from vimiv.utils import log, is_hex, lazy
pyexiv2 = lazy.import_module("pyexiv2", optional=True)
_logger = log.module_logger(__name__)
class MetadataPyexiv2(metadata.MetadataPlugin):
"""Provides metadata support based on pyexiv2."""
def __init__(self, path: str) -> None:
self._path = path
try:
self._metadata = pyexiv2.ImageMetadata(path)
self._metadata.read()
except FileNotFoundError:
_logger.debug("File %s not found", path)
self._metadata = None
@staticmethod
def name() -> str:
"""Get the name of the used backend."""
return "pyexiv2"
@staticmethod
def version() -> str:
"""Get the version of the used backend."""
return pyexiv2.__version__
def get_metadata(self, desired_keys: Sequence[str]) -> metadata.MetadataDictT:
"""Get value of all desired keys for the current image."""
out = {}
if self._metadata is None:
return {}
for key in desired_keys:
try:
key_name = self._metadata[key].name
try:
key_value = self._metadata[key].human_value
# Not all metadata (i.e. IPTC) provide human_value, take raw_value
except AttributeError:
value = self._metadata[key].raw_value
# For IPTC the raw_value is a list of strings
if isinstance(value, list):
key_value = ", ".join(value)
else:
key_value = value
out[key] = (key_name, key_value)
except KeyError:
_logger.debug("Key %s is invalid for the current image", key)
return out
def get_keys(self) -> Iterable[str]:
"""Get the keys for all metadata values available for the current image."""
if self._metadata is None:
return iter([])
return (key for key in self._metadata if not is_hex(key.rpartition(".")[2]))
def copy_metadata(self, dest: str, reset_orientation: bool = True) -> bool:
"""Copy metadata from the current image to dest image."""
if self._metadata is None:
return False
if reset_orientation:
with contextlib.suppress(KeyError):
self._metadata[
"Exif.Image.Orientation"
] = metadata.ExifOrientation.Normal
try:
dest_image = pyexiv2.ImageMetadata(dest)
dest_image.read()
# File types restrict the metadata type they can store.
# Try copying all types one by one and skip if it fails.
for copy_args in set(itertools.permutations((True, False, False, False))):
with contextlib.suppress(ValueError):
self._metadata.copy(dest_image, *copy_args)
dest_image.write()
return True
except FileNotFoundError:
_logger.debug("Failed to write metadata. Destination '%s' not found", dest)
except OSError as e:
_logger.debug("Failed to write metadata for '%s': '%s'", dest, str(e))
return False
def get_date_time(self) -> str:
"""Get creation date and time of the current image as formatted string."""
if self._metadata is None:
return ""
with contextlib.suppress(KeyError):
return self._metadata["Exif.Image.DateTime"].raw_value
return ""
def init(*_args: Any, **_kwargs: Any) -> None:
"""Initialize pyexiv2 handler if pyexiv2 is available."""
if pyexiv2 is not None:
metadata.register(MetadataPyexiv2)
else:
_logger.warning("Please install py3exiv2 to use this plugin")

View File

@ -7,8 +7,8 @@
"""Image type detection based on magic bytes.
The following table shows which image types can be read by QT natively, while the second
one shows which can be read when having the `qtimageformats` ad-on module installed.
The following table shows which image types can be read by Qt natively, while the second
one shows which can be read when having the `qtimageformats` add-on module installed.
This module allows to detect images of these types based on the magic bytes of the file.
@ -67,7 +67,7 @@ def detect(filename: str) -> Optional[str]:
"""Determine type of image based on the magic bytes.
Evaluates each registered check function in the order they were registered. If
registered with `priority`, then that check is evaluated before all checks with out
registered with `priority`, then that check is evaluated before all checks without
`priority`.
Args:

View File

@ -62,7 +62,6 @@ class QtReader(BaseReader):
self._handler = QImageReader(path, file_format.encode())
self._handler.setAutoTransform(True)
if not self._handler.canRead():
# TODO
raise ValueError(f"'{path}' cannot be read as image")
@classmethod

View File

@ -47,7 +47,7 @@ In case you want to ensure that a log message is only logged a single time, pass
import logging
from typing import Dict, List, Optional, Any, Set
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtCore import pyqtSignal, QObject, QLoggingCategory
import vimiv
@ -111,6 +111,11 @@ def setup_logging(level: int, *debug_modules: str) -> None:
_app_logger.level = level
_app_logger.handlers = [file_handler, console_handler, statusbar_loghandler]
LazyLogger.handlers = [console_handler, file_handler]
# Disable qt logging at higher log levels
if level > logging.ERROR:
QLoggingCategory.setFilterRules("*.warning=false\n*.critical=false")
elif level > logging.WARNING:
QLoggingCategory.setFilterRules("*.warning=false")
# Setup debug logging for specific module loggers
_debug_loggers.extend(debug_modules)
for name, logger in _module_loggers.items():

View File

@ -22,6 +22,7 @@ from PyQt5.QtCore import QRunnable, pyqtSignal, QObject
from PyQt5.QtGui import QIcon, QPixmap, QImage
import vimiv
from vimiv import api
from vimiv.utils import xdg, imagereader, Pool
@ -131,6 +132,24 @@ class ThumbnailCreator(QRunnable):
def _get_source_mtime(path: str) -> int:
return int(os.path.getmtime(path))
def _save_thumbnail(self, image: QImage, thumbnail_path: str) -> None:
"""Save the thumbnail file to the disk.
Args:
image: The QImage representing the thumbnail.
thumbnail_path: Path to which the thumbnail is stored.
Returns:
None.
"""
# First create temporary file and then move it. This avoids
# problems with concurrent access of the thumbnail cache, since
# "move" is an atomic operation
handle, tmp_filename = tempfile.mkstemp(dir=self._manager.directory)
os.close(handle)
os.chmod(tmp_filename, 0o600)
image.save(tmp_filename, format="png")
os.replace(tmp_filename, thumbnail_path)
def _create_thumbnail(self, path: str, thumbnail_path: str) -> QPixmap:
"""Create thumbnail for an image.
@ -153,14 +172,8 @@ class ThumbnailCreator(QRunnable):
return self._manager.fail_pixmap
for key, value in attributes.items():
image.setText(key, value)
# First create temporary file and then move it. This avoids
# problems with concurrent access of the thumbnail cache, since
# "move" is an atomic operation
handle, tmp_filename = tempfile.mkstemp(dir=self._manager.directory)
os.close(handle)
os.chmod(tmp_filename, 0o600)
image.save(tmp_filename, format="png")
os.replace(tmp_filename, thumbnail_path)
if api.settings.thumbnail.save:
self._save_thumbnail(image, thumbnail_path)
return QPixmap(image)
def _get_thumbnail_attributes(self, path: str, image: QImage) -> Dict[str, str]:

View File

@ -18,7 +18,6 @@ from typing import Optional
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR
import vimiv
from vimiv.imutils import exif
from vimiv.utils import xdg, run_qprocess, lazy
QtSvg = lazy.import_module("PyQt5.QtSvg", optional=True)
@ -42,8 +41,6 @@ def info() -> str:
f"Qt: {QT_VERSION_STR}\n"
f"PyQt: {PYQT_VERSION_STR}\n\n"
f"Svg Support: {bool(QtSvg)}\n"
f"Pyexiv2: {exif.pyexiv2.__version__ if exif.pyexiv2 is not None else None}\n"
f"Piexif: {exif.piexif.VERSION if exif.piexif is not None else None}"
)