Compare commits

...

49 Commits

Author SHA1 Message Date
Xaver Hugl
888bca7387 cmake: add cmake config
That way, other projects can declare to have a runtime dependency on KImageFormats
2025-05-21 17:39:44 +02:00
Mirco Miranda
e3aefd2aa1 JXR: Restore device position after reading options 2025-05-14 02:50:03 +02:00
Mirco Miranda
aa8134ee0d README.md: minor fixes and improvements 2025-05-06 07:33:27 +02:00
Laurent Montel
9f09473aa0 It compiles fine without kf_6_13 deprecated methods 2025-05-04 21:31:49 +02:00
Mirco Miranda
62eb1d28cb Add README about autotests 2025-05-03 11:01:46 +02:00
Nicolas Fella
15bece40ec Update version to 6.15.0 2025-05-02 17:10:02 +02:00
Nicolas Fella
1b3d2afbe3 Update dependency version to 6.14.0 2025-05-02 16:02:43 +02:00
Mirco Miranda
850068c1dc JXR: fix compilation error on 32-bit systems 2025-04-22 08:01:02 +02:00
Mirco Miranda
6bf38ea638 Improved EXIF V3 compatibility (2)
This patch improve the string read/write in case of non ASCII encoding.
- When reading, checks for UTF-8 text even on ASCII data type. If it fails, Latin1 converter is used.
- When writing using V3 specs, a better check is done to identify 7-bit ASCII text.

Related to MR !358
2025-04-21 11:29:13 +00:00
Alessandro Astone
2adca7c0ca jxr: Use qsizetype for image size variables
Fixes type mismatch on 32-bit architectures.

/builddir/build/BUILD/kf6-kimageformats-6.13.0-build/kimageformats-6.13.0/src/imageformats/jxr.cpp: In member function ‘virtual bool JXRHandler::read(QImage*)’:
/builddir/build/BUILD/kf6-kimageformats-6.13.0-build/kimageformats-6.13.0/src/imageformats/jxr.cpp:994:88: error: no matching function for call to ‘min(qint64&, qsizetype)’
  994 |                 std::memcpy(img.scanLine(y), ba.data() + convStrideSize * y, (std::min)(convStrideSize, img.bytesPerLine()));
      |                                                                              ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2025-04-21 10:07:00 +02:00
Daniel Novomeský
dd69fdaea9 lib prefix on Android
Qt's original qtimageformats plug-ins use lib prefix too,
without the lib prefix, the image plug-ins don't work on Android.
2025-04-19 14:16:18 +00:00
Mirco Miranda
ae62ea3dfc Improved EXIF V3 compatibility
EXIF specs V3 added the UTF-8 data type. The MicroExif class now allows serializations to choose whether to support the V2 or V3 format. To maximize compatibility with old readers, even when V3 is set, the ASCII data type is used if possible.

The JXR plugin, based on TIFF V6 container, does not allow to use the V3 format (it does not recognize the UTF-8 data type) and therefore V2 has been forced. For all other plugins using MicroExif, it is now possible to save, e.g., descriptions in Japanese.

Please note that this patch is also a bugfix: when saving, version 3 was set but the strings were always saved as ASCII.
2025-04-18 13:53:12 +00:00
Nicolas Fella
9c47845f15 Update version to 6.14.0 2025-04-10 19:53:58 +02:00
Volker Krause
7c7fa73020 Remove conditions for no longer supported Qt versions 2025-04-08 18:46:13 +02:00
Albert Astals Cid
92e4271e84 GIT_SILENT Upgrade Qt6 version requirement to 6.7.0. 2025-04-07 10:13:41 +02:00
Laurent Montel
6f588c6fd3 Add missing include mocs 2025-04-03 07:34:17 +02:00
Laurent Montel
a182478e2c It compiles fine without qt6.9 deprecated methods 2025-03-23 22:52:05 +00:00
Mirco Miranda
4026f41890 PSD: use linear profile on float images
On float images, if not color profile is present, a linear one should be chosen. Photoshop works on 32-bit images in a linear color space.
2025-03-23 22:31:23 +00:00
Laurent Montel
bef2b9168f It compiles fine without kf6.12 deprecated methods 2025-03-22 06:47:20 +01:00
Daniel Novomeský
473f5d9847 Write tests for hej2 format 2025-03-16 21:31:46 +01:00
Daniel Novomeský
9bee29cc01 heif: enable saving of hej2 format 2025-03-16 18:37:48 +01:00
Nicolas Fella
cdf3be3af1 Update dependency version to 6.13.0 2025-03-14 21:31:42 +01:00
Albert Astals Cid
752b18a42c CI: Enable heif so we make sure it compiles 2025-03-12 16:17:29 +00:00
Daniel Novomeský
97a1ea181c writetest: special handling for HEIF format 2025-03-12 16:41:39 +01:00
Daniel Novomeský
64a43fb04f readtest: special handling for HEIF format 2025-03-12 16:17:59 +01:00
Daniel Novomeský
6821c29819 heif: disable AVCI decoder for libheif before 1.19.6 2025-03-12 13:26:42 +01:00
Daniel Novomeský
e4d95c03fa SKIP tests when libheif configuration is incomplete 2025-03-10 22:01:14 +01:00
Daniel Novomeský
afa8ed1a5d heif: enable reading images with native 16 bit depth 2025-03-10 19:42:34 +01:00
Mirco Miranda
245c835d92 Use of heif_context_add_XMP_metadata instead
heif_context_add_XMP_metadata2
2025-03-09 10:44:18 +01:00
Nicolas Fella
b2663d2651 Update dependency version to 6.13.0 2025-03-07 19:00:23 +01:00
Nicolas Fella
35ab37c628 Update dependency version to 6.12.0 2025-03-07 15:09:18 +01:00
Albert Astals Cid
b28baa4a1e sct: qRound with param bigger than max int is undefined
oss-fuzz/399667098
2025-03-04 06:33:52 +00:00
Albert Astals Cid
5d2540c135 sct: Use height instead of width when calculating dotsPerMeterY 2025-03-04 06:29:58 +00:00
Mirco Miranda
25cc8bc262 MicroExif: search for the TIFF signature 2025-03-03 09:17:56 +01:00
Mirco Miranda
7742537f8c MicroExif: API improvements and minor bugfixes 2025-03-02 13:34:02 +00:00
Mirco Miranda
d3386bbf50 Fix compilation error 2025-03-02 13:49:25 +01:00
Mirco Miranda
e77986c7e0 Added support for resolution and EXIF/XMP metadata to HEIF 2025-03-02 08:03:28 +00:00
Mirco Miranda
c0261f4926 JXR: Added rotation (transformation) support
- Full rotation support on load and save.
- Improve also Windows compatibility by converting RGB32 to BGR32 on saving

Images saved with orientation are displayed correctly by Windows Explorer (which natively supports JXR files):

![_BC374A2E-7970-4B72-87BD-68DD3D8FB7AA_](/uploads/2268aa3066d82a4f97d026a64f2b70c2/_BC374A2E-7970-4B72-87BD-68DD3D8FB7AA_.png){width=597 height=259}
2025-02-25 21:37:17 +00:00
Mirco Miranda
e5cf9caac5 JXR: added support to EXIF metadata
Improved metadata support via EXIF ​​metadata. Since JXR is based on a TIFF container, EXIF ​​data is read directly from the file so it always works (even with versions of libjxr that don't have the metadata reading API).

It also solves the following issues:
- Incorrect date format on saved JXR files (was saved in ISO format instead of `yyyy:MM:dd HH:mm:ss`).
- Incorrect date type setting in EXIF ​​data: the `DateTime` tag should be updated on every save (verified by GIMP and Photoshop). Our `CreationDate` metadata is the equivalent of the EXIF ​​`DateTimeOriginal` tag.

Closes #22
2025-02-23 00:38:27 +00:00
Mirco Miranda
90d4256f3d AVIF: added support to XMP and EXIF metadata
Allow to load/save info about:
- GPS info (latitude, longitude, altitude)
- Various text info (title, description, author, copyright, etc...)
- Image resolution

The compatibility of the modifications has been tested with GIMP.
2025-02-19 11:56:19 +00:00
Mirco Miranda
bb1c6aab9e Added pixel limit detected by experimental tests 2025-02-17 08:54:49 +01:00
Daniel Novomeský
74a734efed jxl: fix build with Qt before 6.8.0 2025-02-15 16:16:10 +01:00
Mirco Miranda
e9fa4b6610 JP2: Disable ICC profile writing when saving an image with OpenJPEG V2.5.3 or lesser and improve the format detection on reading. 2025-02-12 06:45:21 +00:00
Daniel Novomeský
36a6ef7d78 heif: improve handling of grayscale ICC profiles 2025-02-10 16:59:49 +00:00
Daniel Novomeský
9fd6896cec Improve printing details when writetest fails 2025-02-10 16:59:49 +00:00
Daniel Novomeský
9b14e752db Update HEIF writetest templates 2025-02-10 16:59:49 +00:00
Laurent Montel
90a2e3b412 GIT_SILENT: it compiles fine without kf6.11 deprecated methods 2025-02-10 06:46:22 +01:00
Mirco Miranda
b28cf4c352 Added JXL to CMYK formats 2025-02-07 15:55:23 +01:00
Nicolas Fella
b536ec4a5e Update version to 6.12.0 2025-02-07 15:38:22 +01:00
98 changed files with 1715 additions and 185 deletions

View File

@ -7,5 +7,5 @@ Dependencies:
Options:
test-before-installing: True
require-passing-tests-on: ['Linux', 'FreeBSD', 'Windows']
cmake-options: "-DKIMAGEFORMATS_DDS=ON -DKIMAGEFORMATS_JXR=ON"
cmake-options: "-DKIMAGEFORMATS_DDS=ON -DKIMAGEFORMATS_JXR=ON -DKIMAGEFORMATS_HEIF=ON"
per-test-timeout: 90

View File

@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.16)
set(KF_VERSION "6.11.0") # handled by release scripts
set(KF_DEP_VERSION "6.11.0") # handled by release scripts
set(KF_VERSION "6.15.0") # handled by release scripts
set(KF_DEP_VERSION "6.14.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.11.0 NO_MODULE)
find_package(ECM 6.14.0 NO_MODULE)
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
@ -21,7 +21,7 @@ include(ECMDeprecationSettings)
include(CheckIncludeFiles)
include(FindPkgConfig)
set(REQUIRED_QT_VERSION 6.6.0)
set(REQUIRED_QT_VERSION 6.7.0)
find_package(Qt6Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(KF6Archive ${KF_DEP_VERSION})
@ -99,8 +99,8 @@ endif()
add_feature_info(LibJXR LibJXR_FOUND "required for the QImage plugin for JPEG XR images")
ecm_set_disabled_deprecation_versions(
QT 6.8.0
KF 6.10.0
QT 6.9.0
KF 6.13.0
)
add_subdirectory(src)
@ -109,6 +109,28 @@ if (BUILD_TESTING)
add_subdirectory(tests)
endif()
set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6ImageFormats")
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion
)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/KF6ImageFormatsConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfig.cmake"
INSTALL_DESTINATION "${CMAKECONFIG_INSTALL_DIR}"
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfigVersion.cmake"
DESTINATION "${CMAKECONFIG_INSTALL_DIR}"
COMPONENT Devel
)
include(ECMFeatureSummary)
ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)

View File

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2025 Xaver Hugl <xaver.hugl@gmail.com>
# SPDX-License-Identifier: BSD-2-Clause
@PACKAGE_INIT@
# empty, because this is plugins for Qt instead of a library to link against

View File

@ -53,8 +53,10 @@ willing to sign the Qt Project contributor agreement, it may be better to
submit the plugin directly to the Qt Project.
To be accepted, contributions must:
- Contain the test images needed to verify that the changes work correctly
- Pass the tests successfully
- Contain the test images needed to verify that the changes work correctly.
- Pass the tests successfully.
For more info about tests, see also [Autotests README](autotests/README.md).
## Duplicated Plugins
@ -125,9 +127,12 @@ About the image:
example a verbal description of the image.
- `Copyright`: Copyright notice of the person or organization that claims
the copyright to the image.
- `CreationDate`: Creation date and time in ISO 8601 format without
milliseconds (e.g. 2024-03-23T15:30:43).
- `CreationDate`: When the image was created or captured. Date and time in
ISO 8601 format without milliseconds (e.g. 2024-03-23T15:30:43). This value
should be kept unchanged when present.
- `Description`: A string that describes the subject of the image.
- `Direction`: Floating-point number indicating the direction of the image
when it was captured in degrees (e.g. 123.3).
- `DocumentName`: The name of the document from which this image was
scanned.
- `HostComputer`: The computer and/or operating system in use at the time
@ -136,6 +141,9 @@ About the image:
north of the equator (e.g. 27.717).
- `Longitude`: Floating-point number indicating the longitude in degrees
east of Greenwich (e.g. 85.317).
- `ModificationDate`: Last modification date and time in ISO 8601 format
without milliseconds (e.g. 2024-03-23T15:30:43). This value should be
updated every time the image is saved.
- `Owner`: Name of the owner of the image.
- `Software`: Name and version number of the software package(s) used to
create the image.
@ -189,6 +197,7 @@ are created automatically:
- `Software`: Created using `applicationName` and `applicationVersion` methods
of [`QCoreApplication`](https://doc.qt.io/qt-6/qcoreapplication.html).
- `CreationDate`: Set to current time and date.
- `ModificationDate`: Set to current time and date.
### ICC profile support
@ -213,7 +222,7 @@ plugin ('n/a' means no limit, i.e. the limit depends on the format encoding).
- EPS: n/a
- HDR: n/a (large image)
- HEIF: n/a
- JP2: 300,000 x 300,000 pixels
- JP2: 300,000 x 300,000 pixels, in any case no larger than 2 gigapixels
- JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels
- JXR: n/a, in any case no larger than 4 GB
- KRA: same size as Qt's PNG plugin
@ -258,7 +267,7 @@ been used or the maximum size of the image that can be saved has been limited.
PSD plugin loads CMYK, Lab and Multichannel images and converts them to RGB
without using the ICC profile.
JP2, JXR, PSD and SCT plugins natively support 4-channel CMYK images when
JP2, JXL, JXR, PSD and SCT plugins natively support 4-channel CMYK images when
compiled with Qt 6.8+.
### The DDS plugin
@ -314,6 +323,7 @@ in your cmake options.**
JP2 plugin has the following limitations due to the lack of support by OpenJPEG:
- Metadata are not supported.
- Image resolution is not supported.
- To write ICC profiles you need OpenJPEG V2.5.4 or higher
### The JXL plugin

View File

@ -122,9 +122,12 @@ if (LibHeif_FOUND)
kimageformats_read_tests(FUZZ 1
hej2
)
kimageformats_write_tests(FUZZ 1
hej2-nodatacheck-lossless
)
endif()
if (LibHeif_VERSION VERSION_GREATER_EQUAL "1.19.0")
if (LibHeif_VERSION VERSION_GREATER_EQUAL "1.19.6")
kimageformats_read_tests(FUZZ 4
avci
)

253
autotests/README.md Normal file
View File

@ -0,0 +1,253 @@
# Autotests
Automated testing for plugins to allow
[`QImage`](https://doc.qt.io/qt-6/qimage.html) to support
extra file formats.
## Introduction
The testing part is essential for the correct functioning of the plugins.
There are generic read/write tests and specific tests for the plugins that
require them.
## Read tests
The generic reading tests are contained in the `read` folder. Inside the
`read` folder, there are one or more folders for each plugin to be tested.
A plugin can support multiple image file types, and if you need different
parameters for each type, you need to create a folder for each type supported
by the plugin (see e.g. HEIF plugin). If all formats supported by the plugin
do not require different parameters, a single folder approach is simpler (see
e.g. PSD plugin).
The reading tests are mainly based on comparing the image read by the plugin
with a template in a known and working format. For this reason, the template
formats chosen are those distributed by the Qt project: PNG in the first
instance and TIFF for image formats not supported by PNG (e.g. CMYK images).
Some image options such as `QImageIOHandler::Size`,
`QImageIOHandler::ImageFormat` and `QImageIOHandler::ImageTransformation` are
also checked. If supported by the plugin, the resulting image is checked to
see if it is compatible with the option's specification.
Optionally, for each image, you can also create a JSON file to modify the test
behavior and/or verify data other than image pixels, such as metadata.
Finally, two tests are run for each test case:
- On a random access device: this test must not fail.
- On a sequential access device: a plugin may not support sequential operation.
In this case, the test is skipped. However, if an image is returned, the test
must succeed.
### The readtest command
To start a test, run the `readtest` command with the format to test as an
argument. The format is one of those supported by plugins and a folder with
the name of the format must be present inside the `read` folder.
Depending on the format, you can specify the following additional options.
- `--help`: Displays help on commandline options.
- `--fuzz <max>`: The fuzziness. Used to add some deviation in ARGB data
(nornally used on lossy codec).
- `--perceptive-fuzz`: Used to scale dynamically the fuzziness based on
the alpha channel value. This is useful on images with pre-multiplied and
small alphas. Qt can use different roundings based on optimizations resulting
in very different RGB values. Since the alpha is small visually there is no
difference (so it is not considered an error).
- `--skip-optional-tests`: Used to skip the optional test such as metadata
and resolution tests.
Note that some tests may fail if the correct options are not used. The correct
options for each test are defined in [CMakeLists.txt](CMakeLists.txt).
See also [Add a test to CMakeLists.txt](#add-a-test-to-cmakeliststxt).
### Test image nomenclature
Each test consists of the image to test, the verification image(s) (template)
and, optionally, the additional JSON file.
To be a test, the names of these files must be the same (except for
the extension). A test for a JXL image would be, for example, composed like
this:
- `testRGB.jxl`: The image to test.
- `testRGB.jxl.json`: The test behavior modifier (note that it must contain
the double extension).
- `testRGB.png`: How the image should look (template). The template name
can be different if specified in the JSON file.
Although there is no precise rule for the name of a test, it is good to have
a name that is explanatory.
### JSON behavior file
The behavior file was initially introduced to solve compatibility issues
between different versions of Qt supported by the framework. It was later
extended to also check image metadata.
The JSON file consists of an array of JSON objects. The objects in the array
are iterated sequentially and the first object that matches the requirements
is used for testing (successes are ignored).
Supported values for JSON objects:
- `comment`: Type string. A string shown by the test when a condition occurs.
- `description`: Type string. A description of the object. Not used by the
test.
- `disableAutoTransform`: Type boolean. By default, tests are run with
autotransform enabled (i.e. rotation is applied if the plugin supports it).
Set to `true` to disable autotransform.
- `filename`: Type string. Name of the template file to use. E.g.
"testRGB_Qt_6_2.png".
- `fuzziness`: Type integer. Set the fuzziness only if not already set on the
command line. The value set on the command line wins over the one in the JSON
file.
- `maxQtVersion`: Type string. Maximum Qt version this object is compatible
with (if not set means all). E.g. "6.2.99".
- `metadata`: Type Array. An array of key/value objects (string type)
containing the image metadata as returned by `QImage::text`.
- `minQtVersion`: Type string. Minimum Qt version this object is compatible
with (if not set means all). E.g. "6.2.0".
- `perceptiveFuzziness` Type boolean. Set the perceptive fuzziness only if not
already set on the command line. The value set on the command line wins over
the one in the JSON file.
- `resolution`: Type object. An object with the `dotsPerMeterX` and
`dotsPerMeterY` (integer) values of the image.
- `seeAlso`: Type string. More info about the object. Normally used to point
to bug reports. Not used by the test.
- `unsupportedFormat`: Type `boolean`. When true, the test is skipped.
Some examples:
- Example 1: [Runs only on Qt without alpha bug on float formats](read/jxl/testcard_rgba_fp16.jxl.json)
- Example 2: [Rotation disabled](read/jxl/orientation6_notranfs.jxl.json)
- Example 3: [Metadata](read/psd/metadata.psd.json)
- Example 4: [Check Qt version, resolution and metadata](read/psd/mch-16bits.psd.json)
- Example 5: [Fuzziness setting](read/xcf/birthday16.xcf.json)
These are just a few examples. More examples can be found in the test folders.
## Write tests
The generic writing tests are contained in the `write/basic` and
`write/format` folders. Similar to the read tests, they verify the written
(and then reread) image with a template.
The write test is composed of several phases:
- Basic test: Uses the `write/basic` folder and checks the most common images
and, optionally, metadata and resolution via a JSON properties file.
- Format test: Uses the `write/format` folder and checks that all QImage image
formats are written correctly.
- Dimensional test: Uses the `write/format` folder and check images of
different sizes (odd numbers, prime numbers, etc.) to verify internal
alignments.
- Null device test: Verify that there are no crashes if the device is null.
### The writetest command
To start a test, run the `writetest` command with the format to test as an
argument. The format is one of those supported by plugins, a folder with
the name of the format must be present inside the `write/format` folder and
may need a template image in `write/basic`.
Depending on the format, you can specify the following additional options.
- `--help`: Displays help on commandline options.
- `--create-format-templates`: Create template images for all formats
supported by the QImage in `write/format`. Command to simplify the creation of
format test images when adding a new plugin or modifying an old one. This
command is not intended to be used from the CMakeLists file as it must be used
manually and the generated images must be verified one by one.
- `--fuzz <max>`: The fuzziness. Used to add some deviation in ARGB data
(nornally used on lossy codec).
- `--lossless`: Check that reading back the data gives the same image.
- `--no-data-check`: Don't check that write data is exactly the same.
- `--skip-optional-tests`: Skip optional data tests (metadata, resolution,
etc...).
Note that some tests may fail if the correct options are not used. The correct
options for each test are defined in [CMakeLists.txt](CMakeLists.txt).
See also [Add a test to CMakeLists.txt](#add-a-test-to-cmakeliststxt).
### JSON properties file
The properties file must be located in `write/basic` and must have the name
of the file format (e.g. jxl.json). It is a JSON object composed of the
following values:
- `format`: Type string. The format tested.
- `metadata`: Type Array. An array of key/value objects (string type)
containing the image metadata as returned by `QImage::text`.
- `resolution`: Type object. An object with the `dotsPerMeterX` and `
dotsPerMeterY` (integer) values of the image.
[This is an example](write/basic/jxl.json) of property file.
## Custom tests
If the generic read/write tests do not meet the requirements of a plugin,
it is possible to write a custom test.
In general it makes sense to write a dedicated test for a format if and
only if you are testing unique features not present in other plugins.
### The PIC test
The PIC test is generated using Qt Test class. For more information
see [Qt Test](https://doc.qt.io/qt-6/qttest-index.html).
### The ANI test
The ANI test is generated using Qt Test class. For more information
see [Qt Test](https://doc.qt.io/qt-6/qttest-index.html).
## Add a test to CMakeLists.txt
To add a test to CMake use the `kimageformats_read_tests` and
`kimageformats_write_tests` functions. For example, to add the read
tests for the PSD you just write `kimageformats_read_tests(psd)`.
It is also possible to pass command line arguments to the test by
appropriately composing the string passed to the test functions.
For boolean parameters you need to add a string starting with '-'
after the image format. For example, to pass `--skip-optional-tests`
to the PSD plugin write `kimageformats_read_tests(psd-skipoptional)`.
To add a fuzziness of 4, you must first set it as follows:
`kimageformats_read_tests(FUZZ 4 psd-skipoptional)`.
The possible modifiers for `kimageformats_read_tests` are as follows:
- `-skipoptional`: Add the `--skip-optional-tests` command line parameter.
The possible modifiers for `kimageformats_write_tests` are as follows:
- `-lossless`: Add the `--lossless` command line parameter.
- `-nodatacheck`: Add the `--no-data-check` command line parameter.
- `-skipoptional`: Add the `--skip-optional-tests` command line parameter.
To set multiple parameters, you can enter multiple modifiers. For example:
```
kimageformats_write_tests(FUZZ 1
hej2-nodatacheck-lossless
)
```
## OSS-Fuzz
Plugins are also tested with [OSS-Fuzz](https://google.github.io/oss-fuzz/)
project to identify possible security issues. When adding a new plugin it is
also necessary to add it to the test in the [kimageformats
project](https://github.com/google/oss-fuzz/tree/master/projects/kimageformats)
on OSS-Fuzz.
## TODO
List of tests not implemented or only partially implemented.
### Color Profiles Test
Many plugins support color profiles via [`QColorSpace`](https://doc.qt.io/qt-6/qcolorspace.html).
Checking for correct color management is increasingly necessary especially now
that monitors are HDR and other than RGB color spaces have been added to Qt.
Furthermore, lossy plugins often have different color management behaviors
depending on how the image is saved.
### Animations Test
Few plugins support animations. There are currently no plans for
implementation.

Binary file not shown.

View File

@ -0,0 +1,59 @@
[
{
"fileName" : "metadata.png",
"metadata" : [
{
"key" : "ModificationDate",
"value" : "2025-02-19T08:27:22+01:00"
},
{
"key" : "Software" ,
"value" : "GIMP 3.0.0-RC3"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"key" : "Description",
"value" : "TV broadcast test image."
},
{
"key" : "Latitude",
"value" : "44.6478"
},
{
"key" : "LensManufacturer",
"value" : "KDE Glasses"
},
{
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "Longitude",
"value" : "10.9254"
},
{
"key" : "Manufacturer",
"value" : "KFramework"
},
{
"key" : "Model",
"value" : "KImageFormats"
}
],
"resolution" : {
"dotsPerMeterX" : 11811,
"dotsPerMeterY" : 5905
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

View File

@ -0,0 +1,59 @@
[
{
"fileName" : "metadata.png",
"metadata" : [
{
"key" : "ModificationDate",
"value" : "2025-02-26T16:52:06Z"
},
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10 (Linux)"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"key" : "Description",
"value" : "TV broadcast test image."
},
{
"key" : "Latitude",
"value" : "44.6478"
},
{
"key" : "LensManufacturer",
"value" : "KDE Glasses"
},
{
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "Longitude",
"value" : "10.9254"
},
{
"key" : "Manufacturer",
"value" : "KFramework"
},
{
"key" : "Model",
"value" : "KImageFormats"
}
],
"resolution" : {
"dotsPerMeterX" : 11811,
"dotsPerMeterY" : 5905
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -3,7 +3,7 @@
"fileName" : "gimp_exif.png",
"metadata" : [
{
"key" : "CreationDate",
"key" : "ModificationDate",
"value" : "2025-01-05T10:18:16"
},
{

Binary file not shown.

View File

@ -0,0 +1,59 @@
[
{
"fileName" : "metadata.png",
"metadata" : [
{
"key" : "CreationDate",
"value" : "2025-01-14T10:34:51Z"
},
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"key" : "Description",
"value" : "TV broadcast test image."
},
{
"key" : "Latitude",
"value" : "44.6478"
},
{
"key" : "LensManufacturer",
"value" : "KDE Glasses"
},
{
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "Longitude",
"value" : "10.9254"
},
{
"key" : "Manufacturer",
"value" : "KFramework"
},
{
"key" : "Model",
"value" : "KImageFormats"
}
],
"resolution" : {
"dotsPerMeterX" : 11811,
"dotsPerMeterY" : 5906
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -8,7 +8,7 @@
},
"metadata" : [
{
"key" : "CreationDate",
"key" : "ModificationDate",
"value" : "2022-11-11T14:27:52"
},
{

View File

@ -8,7 +8,7 @@
},
"metadata" : [
{
"key" : "CreationDate",
"key" : "ModificationDate",
"value" : "2022-11-11T14:27:39"
},
{

View File

@ -3,7 +3,7 @@
"fileName" : "metadata.png",
"metadata" : [
{
"key" : "CreationDate",
"key" : "ModificationDate",
"value" : "2025-01-14T13:53:32+01:00"
},
{

View File

@ -258,6 +258,15 @@ int main(int argc, char **argv)
});
QTextStream(stdout) << "QImageReader::supportedImageFormats: " << formatStrings.join(", ") << "\n";
if (!formats.contains(format)) {
if (format == "avci" || format == "heif" || format == "hej2") {
QTextStream(stdout) << "WARNING : " << suffix << " is not supported with current libheif configuration!\n"
<< "********* "
<< "Finished basic read tests for " << suffix << " images *********\n";
return 0;
}
}
const QFileInfoList lstImgDir = imgdir.entryInfoList();
// Launch 2 runs for each test: first run on a random access device, second run on a sequential access device
for (int seq = 0; seq < 2; ++seq) {
@ -323,7 +332,12 @@ int main(int argc, char **argv)
OptionTest optionTest;
if (!optionTest.store(&inputReader)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": error while reading options\n";
++failed;
if (format == "heif") {
// libheif + ffmpeg decoder is unable to load all HEIF files.
++skipped;
} else {
++failed;
}
continue;
}

View File

@ -0,0 +1,65 @@
{
"format" : "avif",
"metadata" : [
{
"key" : "CreationDate",
"value" : "2025-01-14T13:53:32+01:00"
},
{
"key" : "Direction",
"value" : "123.7"
},
{
"key" : "ModificationDate",
"value" : "2025-02-14T15:58:44+01:00"
},
{
"key" : "Software" ,
"value" : "Adobe Photoshop 26.2 (Windows)"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"key" : "Description",
"value" : "テレビ放送テスト映像。(TV broadcast test image.)"
},
{
"key" : "Latitude",
"value" : "44.6478"
},
{
"key" : "LensManufacturer",
"value" : "KDE Glasses"
},
{
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "Longitude",
"value" : "10.9254"
},
{
"key" : "Manufacturer",
"value" : "KFramework"
},
{
"key" : "Model",
"value" : "KImageFormats"
}
],
"resolution" : {
"dotsPerMeterX" : 11811,
"dotsPerMeterY" : 11812
}
}

View File

@ -0,0 +1,65 @@
{
"format" : "heif",
"metadata" : [
{
"key" : "CreationDate",
"value" : "2025-01-14T13:53:32+01:00"
},
{
"key" : "Direction",
"value" : "123.7"
},
{
"key" : "ModificationDate",
"value" : "2025-02-14T15:58:44+01:00"
},
{
"key" : "Software" ,
"value" : "Adobe Photoshop 26.2 (Windows)"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"key" : "Description",
"value" : "テレビ放送テスト映像。(TV broadcast test image.)"
},
{
"key" : "Latitude",
"value" : "44.6478"
},
{
"key" : "LensManufacturer",
"value" : "KDE Glasses"
},
{
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "Longitude",
"value" : "10.9254"
},
{
"key" : "Manufacturer",
"value" : "KFramework"
},
{
"key" : "Model",
"value" : "KImageFormats"
}
],
"resolution" : {
"dotsPerMeterX" : 11811,
"dotsPerMeterY" : 11812
}
}

View File

@ -0,0 +1,65 @@
{
"format" : "hej2",
"metadata" : [
{
"key" : "CreationDate",
"value" : "2025-01-14T13:53:32+01:00"
},
{
"key" : "Direction",
"value" : "123.7"
},
{
"key" : "ModificationDate",
"value" : "2025-02-14T15:58:44+01:00"
},
{
"key" : "Software" ,
"value" : "Adobe Photoshop 26.2 (Windows)"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"key" : "Description",
"value" : "TV broadcast test image."
},
{
"key" : "Latitude",
"value" : "44.6478"
},
{
"key" : "LensManufacturer",
"value" : "KDE Glasses"
},
{
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "Longitude",
"value" : "10.9254"
},
{
"key" : "Manufacturer",
"value" : "KFramework"
},
{
"key" : "Model",
"value" : "KImageFormats"
}
],
"resolution" : {
"dotsPerMeterX" : 11811,
"dotsPerMeterY" : 11812
}
}

View File

@ -5,6 +5,14 @@
"key" : "CreationDate",
"value" : "2025-01-14T13:53:32+01:00"
},
{
"key" : "Direction",
"value" : "123.7"
},
{
"key" : "ModificationDate",
"value" : "2025-02-14T15:58:44+01:00"
},
{
"key" : "Software" ,
"value" : "Adobe Photoshop 26.2 (Windows)"
@ -23,7 +31,7 @@
},
{
"key" : "Description",
"value" : "TV broadcast test image."
"value" : "テレビ放送テスト映像。(TV broadcast test image.)"
},
{
"key" : "Latitude",

View File

@ -5,6 +5,18 @@
"key" : "CreationDate",
"value" : "2025-01-14T13:53:32+01:00"
},
{
"key" : "Direction",
"value" : "123.7"
},
{
"key" : "ModificationDate",
"value" : "2025-02-14T15:58:44+01:00"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Author",
"value" : "KDE Project"
@ -17,6 +29,22 @@
"key" : "Description",
"value" : "TV broadcast test image."
},
{
"key" : "Latitude",
"value" : "44.6478"
},
{
"key" : "LensManufacturer",
"value" : "KDE Glasses"
},
{
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "Longitude",
"value" : "10.9254"
},
{
"key" : "Manufacturer",
"value" : "KFramework"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -46,11 +46,11 @@ QJsonObject readOptionalInfo(const QString &suffix)
return doc.object();
}
void setOptionalInfo(QImage& image, const QString &suffix)
void setOptionalInfo(QImage &image, const QString &suffix)
{
auto obj = readOptionalInfo(suffix);
if (obj.isEmpty()) {
return ;
return;
}
// Set resolution
@ -70,7 +70,7 @@ void setOptionalInfo(QImage& image, const QString &suffix)
}
}
bool checkOptionalInfo(QImage& image, const QString &suffix)
bool checkOptionalInfo(QImage &image, const QString &suffix)
{
auto obj = readOptionalInfo(suffix);
if (obj.isEmpty()) {
@ -258,7 +258,6 @@ QImage formatSourceImage(const QImage::Format &format)
auto folder = QStringLiteral("%1/format/_images").arg(IMAGEDIR);
switch (format) {
case QImage::Format_MonoLSB:
case QImage::Format_Mono:
image = QImage(QStringLiteral("%1/mono.png").arg(folder));
@ -371,7 +370,12 @@ int formatTest(const QString &suffix, bool createTemplates)
QBuffer buffer(&ba);
auto writtenImage = QImageReader(&buffer, suffix.toLatin1()).read();
if (writtenImage.isNull()) {
++failed;
if (suffix.toLatin1() == "heif") {
// libheif + ffmpeg decoder is unable to load all HEIF files.
++skipped;
} else {
++failed;
}
QTextStream(stdout) << "FAIL : error while reading the image " << formatName << "\n";
continue;
}
@ -403,8 +407,16 @@ int formatTest(const QString &suffix, bool createTemplates)
// checking the format: must be the same
if (writtenImage.format() != tmplImage.format()) {
++failed;
auto writtenformatName = QString(QMetaEnum::fromType<QImage::Format>().valueToKey(writtenImage.format()));
auto tmplformatName = QString(QMetaEnum::fromType<QImage::Format>().valueToKey(tmplImage.format()));
QTextStream(stdout) << "FAIL : format mismatch " << formatName << " != " << tmplformatName << "\n";
QTextStream(stdout) << "FAIL : format mismatch " << formatName << " (";
if (format == writtenImage.format()) {
QTextStream(stdout) << "template image: " << tmplformatName << ")\n";
} else if (format == tmplImage.format()) {
QTextStream(stdout) << "written image: " << writtenformatName << ")\n";
} else {
QTextStream(stdout) << writtenformatName << " != " << tmplformatName << ")\n";
}
continue;
}
@ -554,7 +566,7 @@ int nullDeviceTest(const QString &suffix)
writer.optimizedWrite();
writer.progressiveScanWrite();
if (failed == 0) {// success
if (failed == 0) { // success
++passed;
}
@ -586,8 +598,7 @@ int main(int argc, char **argv)
QStringLiteral("max"));
QCommandLineOption createFormatTempates({QStringLiteral("create-format-templates")},
QStringLiteral("Create template images for all formats supported by QImage."));
QCommandLineOption skipOptTest({QStringLiteral("skip-optional-tests")},
QStringLiteral("Skip optional data tests (metadata, resolution, etc.)."));
QCommandLineOption skipOptTest({QStringLiteral("skip-optional-tests")}, QStringLiteral("Skip optional data tests (metadata, resolution, etc.)."));
parser.addOption(lossless);
parser.addOption(ignoreDataCheck);
@ -616,8 +627,28 @@ int main(int argc, char **argv)
}
}
// run test
auto suffix = args.at(0);
// skip test if libheif configuration is obviously incomplete
QByteArray format = suffix.toLatin1();
const QList<QByteArray> read_formats = QImageReader::supportedImageFormats();
const QList<QByteArray> write_formats = QImageWriter::supportedImageFormats();
if (!read_formats.contains(format)) {
if (format == "heif" || format == "hej2") {
QTextStream(stdout) << "WARNING : libheif configuration is missing necessary decoder(s)!\n";
return 0;
}
}
if (!write_formats.contains(format)) {
if (format == "heif" || format == "hej2") {
QTextStream(stdout) << "WARNING : libheif configuration is missing necessary encoder(s)!\n";
return 0;
}
}
// run test
auto ret = basicTest(suffix, parser.isSet(lossless), parser.isSet(ignoreDataCheck), parser.isSet(skipOptTest), fuzzarg);
if (ret == 0) {
ret = formatTest(suffix, parser.isSet(createFormatTempates));

View File

@ -15,6 +15,16 @@ function(kimageformats_add_plugin plugin)
target_sources(${plugin} PRIVATE ${KIF_ADD_PLUGIN_SOURCES})
set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats)
target_link_libraries(${plugin} PRIVATE Qt6::Gui)
if(ANDROID)
# Plugins should be named with lib prefix on Android
# Working name: libplugins_imageformats_kimg_avif_armeabi-v7a.so
# Doesn't work: plugins_imageformats_kimg_avif_armeabi-v7a.so
if(NOT ${CMAKE_SHARED_LIBRARY_PREFIX} STREQUAL "")
set_target_properties(${plugin} PROPERTIES PREFIX ${CMAKE_SHARED_LIBRARY_PREFIX})
endif()
endif()
install(TARGETS ${plugin} DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/imageformats)
endfunction()
@ -25,7 +35,7 @@ kimageformats_add_plugin(kimg_ani SOURCES ani.cpp)
##################################
if (TARGET avif)
kimageformats_add_plugin(kimg_avif SOURCES "avif.cpp")
kimageformats_add_plugin(kimg_avif SOURCES avif.cpp microexif.cpp)
target_link_libraries(kimg_avif PRIVATE "avif")
endif()
@ -68,7 +78,7 @@ kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
##################################
if (LibHeif_FOUND)
kimageformats_add_plugin(kimg_heif SOURCES heif.cpp)
kimageformats_add_plugin(kimg_heif SOURCES heif.cpp microexif.cpp)
target_link_libraries(kimg_heif PRIVATE PkgConfig::LibHeif)
endif()
@ -140,7 +150,7 @@ endif()
##################################
if (LibJXR_FOUND)
kimageformats_add_plugin(kimg_jxr SOURCES jxr.cpp)
kimageformats_add_plugin(kimg_jxr SOURCES jxr.cpp microexif.cpp)
kde_enable_exceptions()
target_include_directories(kimg_jxr PRIVATE ${LIBJXR_INCLUDE_DIRS})
target_link_libraries(kimg_jxr PRIVATE ${LIBJXR_LIBRARIES})

View File

@ -12,6 +12,7 @@
#include <QColorSpace>
#include "avif_p.h"
#include "microexif_p.h"
#include "util_p.h"
#include <cfloat>
@ -151,9 +152,6 @@ bool QAVIFHandler::ensureDecoder()
m_decoder = avifDecoderCreate();
m_decoder->ignoreExif = AVIF_TRUE;
m_decoder->ignoreXMP = AVIF_TRUE;
#if AVIF_VERSION >= 80400
m_decoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
#endif
@ -534,12 +532,49 @@ bool QAVIFHandler::decode_one_frame()
m_current_image.setColorSpace(colorspace);
if (m_decoder->image->exif.size) {
auto exif = MicroExif::fromRawData(reinterpret_cast<const char *>(m_decoder->image->exif.data), m_decoder->image->exif.size);
exif.updateImageResolution(m_current_image);
exif.updateImageMetadata(m_current_image);
}
if (m_decoder->image->xmp.size) {
auto ba = QByteArray::fromRawData(reinterpret_cast<const char *>(m_decoder->image->xmp.data), m_decoder->image->xmp.size);
m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(ba));
}
m_estimated_dimensions = m_current_image.size();
m_must_jump_to_next_image = false;
return true;
}
static void setMetadata(avifImage *avif, const QImage& image)
{
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
if (!xmp.isEmpty()) {
#if AVIF_VERSION >= 1000000
auto res = avifImageSetMetadataXMP(avif, reinterpret_cast<const uint8_t *>(xmp.constData()), xmp.size());
if (res != AVIF_RESULT_OK) {
qWarning("ERROR in avifImageSetMetadataXMP: %s", avifResultToString(res));
}
#else
avifImageSetMetadataXMP(avif, reinterpret_cast<const uint8_t *>(xmp.constData()), xmp.size());
#endif
}
auto exif = MicroExif::fromImage(image).toByteArray();
if (!exif.isEmpty()) {
#if AVIF_VERSION >= 1000000
auto res = avifImageSetMetadataExif(avif, reinterpret_cast<const uint8_t *>(exif.constData()), exif.size());
if (res != AVIF_RESULT_OK) {
qWarning("ERROR in avifImageSetMetadataExif: %s", avifResultToString(res));
}
#else
avifImageSetMetadataExif(avif, reinterpret_cast<const uint8_t *>(exif.constData()), exif.size());
#endif
}
}
bool QAVIFHandler::read(QImage *image)
{
if (!ensureOpened()) {
@ -689,6 +724,8 @@ bool QAVIFHandler::write(const QImage &image)
#else
avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
#endif
// set EXIF and XMP metadata
setMetadata(avif, tmpgrayimage);
if (tmpgrayimage.colorSpace().isValid()) {
avif->colorPrimaries = (avifColorPrimaries)1;
@ -915,6 +952,9 @@ bool QAVIFHandler::write(const QImage &image)
avif->colorPrimaries = primaries_to_save;
avif->transferCharacteristics = transfer_to_save;
// set EXIF and XMP metadata
setMetadata(avif, tmpcolorimage);
if (iccprofile.size() > 0) {
#if AVIF_VERSION >= 1000000
res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());

View File

@ -2557,3 +2557,5 @@ QImageIOHandler *QDDSPlugin::create(QIODevice *device, const QByteArray &format)
handler->setFormat(format);
return handler;
}
#include "moc_dds_p.cpp"

View File

@ -8,6 +8,7 @@
*/
#include "heif_p.h"
#include "microexif_p.h"
#include "util_p.h"
#include <libheif/heif.h>
@ -15,14 +16,22 @@
#include <QDebug>
#include <QPointF>
#include <QSysInfo>
#include <cstring>
#include <limits>
#include <string.h>
#ifndef HEIF_MAX_METADATA_SIZE
/*!
* XMP and EXIF maximum size.
*/
#define HEIF_MAX_METADATA_SIZE (4 * 1024 * 1024)
#endif
size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false;
bool HEIFHandler::m_heif_encoder_available = false;
bool HEIFHandler::m_hej2_decoder_available = false;
bool HEIFHandler::m_hej2_encoder_available = false;
bool HEIFHandler::m_avci_decoder_available = false;
extern "C" {
@ -146,6 +155,14 @@ bool HEIFHandler::write_helper(const QImage &image)
break;
}
heif_compression_format encoder_codec = heif_compression_HEVC;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (format() == "hej2") {
encoder_codec = heif_compression_JPEG2000;
save_depth = 8; // for compatibility reasons
}
#endif
heif_chroma chroma;
if (save_depth > 8) {
if (save_alpha) {
@ -278,7 +295,7 @@ bool HEIFHandler::write_helper(const QImage &image)
}
struct heif_encoder *encoder = nullptr;
err = heif_context_get_encoder_for_format(context, heif_compression_HEVC, &encoder);
err = heif_context_get_encoder_for_format(context, encoder_codec, &encoder);
if (err.code) {
qWarning() << "Unable to get an encoder instance:" << err.message;
heif_image_release(h_image);
@ -305,7 +322,25 @@ bool HEIFHandler::write_helper(const QImage &image)
}
}
err = heif_context_encode_image(context, h_image, encoder, encoder_options, nullptr);
struct heif_image_handle *handle;
err = heif_context_encode_image(context, h_image, encoder, encoder_options, &handle);
// exif metadata
if (err.code == heif_error_Ok) {
auto exif = MicroExif::fromImage(tmpimage);
if (!exif.isEmpty()) {
auto ba = exif.toByteArray();
err = heif_context_add_exif_metadata(context, handle, ba.constData(), ba.size());
}
}
// xmp metadata
if (err.code == heif_error_Ok) {
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE));
if (!xmp.isEmpty()) {
auto ba = xmp.toUtf8();
err = heif_context_add_XMP_metadata(context, handle, ba.constData(), ba.size());
}
}
if (encoder_options) {
heif_encoding_options_free(encoder_options);
@ -527,7 +562,7 @@ bool HEIFHandler::ensureDecoder()
QImage::Format target_image_format;
if (bit_depth == 10 || bit_depth == 12) {
if (bit_depth == 10 || bit_depth == 12 || bit_depth == 16) {
if (hasAlphaChannel) {
chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
target_image_format = QImage::Format_RGBA64;
@ -619,6 +654,35 @@ bool HEIFHandler::ensureDecoder()
}
switch (bit_depth) {
case 16:
if (hasAlphaChannel) {
for (int y = 0; y < imageHeight; y++) {
memcpy(m_current_image.scanLine(y), src + (y * stride), 8 * size_t(imageWidth));
}
} else { // no alpha channel
for (int y = 0; y < imageHeight; y++) {
const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
for (int x = 0; x < imageWidth; x++) {
// R
*dest_data = *src_word;
src_word++;
dest_data++;
// G
*dest_data = *src_word;
src_word++;
dest_data++;
// B
*dest_data = *src_word;
src_word++;
dest_data++;
// X = 0xffff
*dest_data = 0xffff;
dest_data++;
}
}
}
break;
case 12:
if (hasAlphaChannel) {
for (int y = 0; y < imageHeight; y++) {
@ -794,10 +858,40 @@ bool HEIFHandler::ensureDecoder()
if (err.code) {
qWarning() << "icc profile loading failed";
} else {
m_current_image.setColorSpace(QColorSpace::fromIccProfile(ba));
if (!m_current_image.colorSpace().isValid()) {
QColorSpace colorspace = QColorSpace::fromIccProfile(ba);
if (!colorspace.isValid()) {
qWarning() << "HEIC image has Qt-unsupported or invalid ICC profile!";
}
#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0))
else if (colorspace.colorModel() == QColorSpace::ColorModel::Cmyk) {
qWarning("CMYK ICC profile is not expected for HEIF, discarding the ICCprofile!");
colorspace = QColorSpace();
} else if (colorspace.colorModel() == QColorSpace::ColorModel::Gray) {
if (hasAlphaChannel) {
QPointF gray_whitePoint = colorspace.whitePoint();
if (gray_whitePoint.isNull()) {
gray_whitePoint = QPointF(0.3127f, 0.329f);
}
const QPointF redP(0.64f, 0.33f);
const QPointF greenP(0.3f, 0.6f);
const QPointF blueP(0.15f, 0.06f);
QColorSpace::TransferFunction trc_new = colorspace.transferFunction();
float gamma_new = colorspace.gamma();
if (trc_new == QColorSpace::TransferFunction::Custom) {
trc_new = QColorSpace::TransferFunction::SRgb;
}
colorspace = QColorSpace(gray_whitePoint, redP, greenP, blueP, trc_new, gamma_new);
if (!colorspace.isValid()) {
qWarning("HEIF plugin created invalid QColorSpace!");
}
} else { // no alpha channel
m_current_image.convertTo(bit_depth > 8 ? QImage::Format_Grayscale16 : QImage::Format_Grayscale8);
}
}
#endif
m_current_image.setColorSpace(colorspace);
}
} else {
qWarning() << "icc profile is empty or above limits";
@ -874,6 +968,38 @@ bool HEIFHandler::ensureDecoder()
m_current_image.setColorSpace(QColorSpace(QColorSpace::SRgb));
}
// read metadata
if (auto numMetadata = heif_image_handle_get_number_of_metadata_blocks(handle, nullptr)) {
QVector<heif_item_id> ids(numMetadata);
heif_image_handle_get_list_of_metadata_block_IDs(handle, nullptr, ids.data(), numMetadata);
for (int n = 0; n < numMetadata; ++n) {
auto itemtype = heif_image_handle_get_metadata_type(handle, ids[n]);
auto contenttype = heif_image_handle_get_metadata_content_type(handle, ids[n]);
auto isExif = !std::strcmp(itemtype, "Exif");
auto isXmp = !std::strcmp(contenttype, "application/rdf+xml");
if (isExif || isXmp) {
auto sz = heif_image_handle_get_metadata_size(handle, ids[n]);
if (sz == 0 || sz >= HEIF_MAX_METADATA_SIZE)
continue;
QByteArray ba(sz, char());
auto err = heif_image_handle_get_metadata(handle, ids[n], ba.data());
if (err.code != heif_error_Ok) {
qWarning() << "Error while reading metadata" << err.message;
continue;
}
if (isXmp) {
m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(ba));
} else if (isExif) {
auto exif = MicroExif::fromByteArray(ba, true);
if (!exif.isEmpty()) {
exif.updateImageResolution(m_current_image);
exif.updateImageMetadata(m_current_image);
}
}
}
}
}
heif_image_release(img);
heif_image_handle_release(handle);
heif_context_free(ctx);
@ -902,6 +1028,13 @@ bool HEIFHandler::isHej2DecoderAvailable()
return m_hej2_decoder_available;
}
bool HEIFHandler::isHej2EncoderAvailable()
{
HEIFHandler::queryHeifLib();
return m_hej2_encoder_available;
}
bool HEIFHandler::isAVCIDecoderAvailable()
{
HEIFHandler::queryHeifLib();
@ -924,8 +1057,9 @@ void HEIFHandler::queryHeifLib()
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
m_hej2_encoder_available = heif_have_encoder_for_format(heif_compression_JPEG2000);
#endif
#if LIBHEIF_HAVE_VERSION(1, 19, 0)
#if LIBHEIF_HAVE_VERSION(1, 19, 6)
m_avci_decoder_available = heif_have_decoder_for_format(heif_compression_AVC);
#endif
m_plugins_queried = true;
@ -992,6 +1126,9 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
if (HEIFHandler::isHej2DecoderAvailable()) {
format_cap |= CanRead;
}
if (HEIFHandler::isHej2EncoderAvailable()) {
format_cap |= CanWrite;
}
return format_cap;
}
@ -1021,7 +1158,7 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
}
}
if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) {
if (device->isWritable() && (HEIFHandler::isHeifEncoderAvailable() || HEIFHandler::isHej2EncoderAvailable())) {
cap |= CanWrite;
}
return cap;

View File

@ -31,6 +31,7 @@ public:
static bool isHeifDecoderAvailable();
static bool isHeifEncoderAvailable();
static bool isHej2DecoderAvailable();
static bool isHej2EncoderAvailable();
static bool isAVCIDecoderAvailable();
static bool isSupportedBMFFType(const QByteArray &header);
@ -62,6 +63,7 @@ private:
static bool m_heif_decoder_available;
static bool m_heif_encoder_available;
static bool m_hej2_decoder_available;
static bool m_hej2_encoder_available;
static bool m_avci_decoder_available;
static QMutex &getHEIFHandlerMutex();

View File

@ -27,6 +27,13 @@
#define JP2_MAX_IMAGE_HEIGHT JP2_MAX_IMAGE_WIDTH
#endif
/* *** JP2_MAX_IMAGE_PIXELS ***
* OpenJPEG seems limited to an image of 2 gigapixel size.
*/
#ifndef JP2_MAX_IMAGE_PIXELS
#define JP2_MAX_IMAGE_PIXELS std::numeric_limits<qint32>::max()
#endif
/* *** JP2_ENABLE_HDR ***
* Enable float image formats. Disabled by default
* due to lack of test images.
@ -101,11 +108,20 @@ public:
JP2HandlerPrivate()
: m_jp2_stream(nullptr)
, m_jp2_image(nullptr)
, m_jp2_version(0)
, m_jp2_codec(nullptr)
, m_quality(-1)
, m_subtype(JP2_SUBTYPE)
{
auto sver = QString::fromLatin1(QByteArray(opj_version())).split(QChar(u'.'));
if (sver.size() == 3) {
bool ok1, ok2, ok3;
auto v1 = sver.at(0).toInt(&ok1);
auto v2 = sver.at(1).toInt(&ok2);
auto v3 = sver.at(2).toInt(&ok3);
if (ok1 && ok2 && ok3)
m_jp2_version = QT_VERSION_CHECK(v1, v2, v3);
}
}
~JP2HandlerPrivate()
{
@ -323,6 +339,11 @@ public:
return false;
}
if (qint64(width) * qint64(height) > JP2_MAX_IMAGE_PIXELS) {
qCritical() << "Maximum image size is limited to" << JP2_MAX_IMAGE_PIXELS << "pixels";
return false;
}
// OpenJPEG uses a shadow copy @32-bit/channel so we need to do a check
auto maxBytes = qint64(QImageReader::allocationLimit()) * 1024 * 1024;
auto neededBytes = qint64(width) * height * nchannels * 4;
@ -363,6 +384,17 @@ public:
prec = 0;
}
auto jp2cs = m_jp2_image->color_space;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if (jp2cs == OPJ_CLRSPC_UNKNOWN || jp2cs == OPJ_CLRSPC_UNSPECIFIED) {
auto cs = colorSpace();
if (cs.colorModel() == QColorSpace::ColorModel::Cmyk)
jp2cs = OPJ_CLRSPC_CMYK;
else if (cs.colorModel() == QColorSpace::ColorModel::Rgb)
jp2cs = OPJ_CLRSPC_SRGB;
else if (cs.colorModel() == QColorSpace::ColorModel::Gray)
jp2cs = OPJ_CLRSPC_GRAY;
}
#endif
if (jp2cs == OPJ_CLRSPC_UNKNOWN || jp2cs == OPJ_CLRSPC_UNSPECIFIED) {
if (m_jp2_image->numcomps == 1)
jp2cs = OPJ_CLRSPC_GRAY;
@ -453,13 +485,28 @@ public:
return subType() == J2K_SUBTYPE ? OPJ_CODEC_J2K : OPJ_CODEC_JP2;
}
/*!
* \brief opjVersion
* \return The runtime library version.
*/
qint32 opjVersion() const
{
return m_jp2_version;
}
bool imageToJp2(const QImage &image)
{
auto ncomp = image.hasAlphaChannel() ? 4 : 3;
auto prec = 8;
auto cs = OPJ_CLRSPC_SRGB;
auto convFormat = image.format();
auto isFloat = false;
auto cs = OPJ_CLRSPC_SRGB;
if (opjVersion() >= QT_VERSION_CHECK(2, 5, 4)) {
auto ics = image.colorSpace();
if (!(ics.isValid() && ics.primaries() == QColorSpace::Primaries::SRgb && ics.transferFunction() == QColorSpace::TransferFunction::SRgb)) {
cs = OPJ_CLRSPC_UNKNOWN;
}
}
switch (image.format()) {
case QImage::Format_Mono:
@ -527,7 +574,7 @@ public:
break;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
case QImage::Format_CMYK8888: // requires OpenJPEG 2.5.3+
if (strcmp(opj_version(), "2.5.3") >= 0) {
if (opjVersion() >= QT_VERSION_CHECK(2, 5, 3)) {
ncomp = 4;
cs = OPJ_CLRSPC_CMYK;
break;
@ -613,8 +660,7 @@ public:
}
}
// With SRGB, Gray and CMYK, writing the colorspace gives an assert
if (cs == OPJ_CLRSPC_UNKNOWN || cs == OPJ_CLRSPC_UNSPECIFIED) {
if (opjVersion() >= QT_VERSION_CHECK(2, 5, 4)) {
auto colorSpace = scl.targetColorSpace().iccProfile();
if (!colorSpace.isEmpty()) {
m_jp2_image->icc_profile_buf = reinterpret_cast<OPJ_BYTE *>(malloc(colorSpace.size()));
@ -674,6 +720,8 @@ private:
opj_image_t *m_jp2_image;
qint32 m_jp2_version;
// read
opj_codec_t *m_jp2_codec;

View File

@ -20,7 +20,7 @@
#include <string.h>
// Avoid rotation on buggy Qts (see also https://bugreports.qt.io/browse/QTBUG-126575)
#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 7) && QT_VERSION < QT_VERSION_CHECK(6, 6, 0)) || (QT_VERSION >= QT_VERSION_CHECK(6, 7, 3))
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 3)
#ifndef JXL_QT_AUTOTRANSFORM
#define JXL_QT_AUTOTRANSFORM
#endif
@ -791,13 +791,8 @@ bool QJpegXLHandler::decode_one_frame()
if (!m_exif.isEmpty()) {
auto exif = MicroExif::fromByteArray(m_exif);
// set image resolution
if (exif.horizontalResolution() > 0)
m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000));
if (exif.verticalResolution() > 0)
m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000));
// set image metadata
exif.toImageMetadata(m_current_image);
exif.updateImageResolution(m_current_image);
exif.updateImageMetadata(m_current_image);
}
m_next_image_delay = m_framedelays[m_currentimage_index];
@ -1329,7 +1324,10 @@ bool QJpegXLHandler::write(const QImage &image)
output_info.uses_original_profile = JXL_FALSE;
if (tmpimage.colorSpace().isValid()) {
const QPointF whiteP = image.colorSpace().whitePoint();
QPointF whiteP(0.3127f, 0.329f);
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
whiteP = image.colorSpace().whitePoint();
#endif
switch (tmpimage.colorSpace().primaries()) {
case QColorSpace::Primaries::SRgb:
@ -1358,6 +1356,9 @@ bool QJpegXLHandler::write(const QImage &image)
break;
case QColorSpace::Primaries::ProPhotoRgb:
color_profile.white_point = JXL_WHITE_POINT_CUSTOM;
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
whiteP = QPointF(0.3457f, 0.3585f);
#endif
color_profile.white_point_xy[0] = whiteP.x();
color_profile.white_point_xy[1] = whiteP.y();
color_profile.primaries = JXL_PRIMARIES_CUSTOM;
@ -1368,6 +1369,7 @@ bool QJpegXLHandler::write(const QImage &image)
color_profile.primaries_blue_xy[0] = 0.0366;
color_profile.primaries_blue_xy[1] = 0.0001;
break;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
case QColorSpace::Primaries::Bt2020:
color_profile.white_point = JXL_WHITE_POINT_D65;
color_profile.primaries = JXL_PRIMARIES_2100;
@ -1378,6 +1380,7 @@ bool QJpegXLHandler::write(const QImage &image)
color_profile.primaries_blue_xy[0] = 0.131;
color_profile.primaries_blue_xy[1] = 0.046;
break;
#endif
default:
if (is_gray && !whiteP.isNull()) {
color_profile.white_point = JXL_WHITE_POINT_CUSTOM;

View File

@ -15,6 +15,7 @@
*/
#include "jxr_p.h"
#include "microexif_p.h"
#include "util_p.h"
#include <QColorSpace>
@ -83,9 +84,12 @@ Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
class JXRHandlerPrivate : public QSharedData
{
private:
QSharedPointer<QTemporaryDir> tempDir;
mutable QSharedPointer<QFile> jxrFile;
mutable QHash<QString, QString> txtMeta;
QSharedPointer<QTemporaryDir> m_tempDir;
QSharedPointer<QFile> m_jxrFile;
MicroExif m_exif;
qint32 m_quality;
QImageIOHandler::Transformations m_transformations;
mutable QHash<QString, QString> m_txtMeta;
public:
PKFactory *pFactory = nullptr;
@ -94,8 +98,10 @@ public:
PKImageEncode *pEncoder = nullptr;
JXRHandlerPrivate()
: m_quality(-1)
, m_transformations(QImageIOHandler::TransformationNone)
{
tempDir = QSharedPointer<QTemporaryDir>(new QTemporaryDir);
m_tempDir = QSharedPointer<QTemporaryDir>(new QTemporaryDir);
if (PKCreateFactory(&pFactory, PK_SDK_VERSION) == WMP_errSuccess) {
PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION);
}
@ -123,9 +129,101 @@ public:
QString fileName() const
{
return jxrFile->fileName();
return m_jxrFile->fileName();
}
/*!
* \brief setQuality
* Set the image quality (write only)
* \param q
*/
void setQuality(qint32 q)
{
m_quality = q;
}
qint32 quality() const
{
return m_quality;
}
/*!
* \brief setTransformation
* Set the image transformation (read/write)
* \param t
*/
void setTransformation(const QImageIOHandler::Transformations& t)
{
m_transformations = t;
}
QImageIOHandler::Transformations transformation() const
{
return m_transformations;
}
static QImageIOHandler::Transformations orientationToTransformation(const ORIENTATION& o)
{
auto v = QImageIOHandler::TransformationNone;
switch (o) {
case O_FLIPV:
v = QImageIOHandler::TransformationFlip;
break;
case O_FLIPH:
v = QImageIOHandler::TransformationMirror;
break;
case O_FLIPVH:
v = QImageIOHandler::TransformationRotate180;
break;
case O_RCW:
v = QImageIOHandler::TransformationRotate90;
break;
case O_RCW_FLIPH:
v = QImageIOHandler::TransformationFlipAndRotate90;
break;
case O_RCW_FLIPV:
v = QImageIOHandler::TransformationMirrorAndRotate90;
break;
case O_RCW_FLIPVH:
v = QImageIOHandler::TransformationRotate270;
break;
default:
v = QImageIOHandler::TransformationNone;
break;
}
return v;
}
static ORIENTATION transformationToOrientation(const QImageIOHandler::Transformations& t)
{
auto v = O_NONE;
switch (t) {
case QImageIOHandler::TransformationFlip:
v = O_FLIPV;
break;
case QImageIOHandler::TransformationMirror:
v = O_FLIPH;
break;
case QImageIOHandler::TransformationRotate180:
v = O_FLIPVH;
break;
case QImageIOHandler::TransformationRotate90:
v = O_RCW;
break;
case QImageIOHandler::TransformationFlipAndRotate90:
v = O_RCW_FLIPH;
break;
case QImageIOHandler::TransformationMirrorAndRotate90:
v = O_RCW_FLIPV;
break;
case QImageIOHandler::TransformationRotate270:
v = O_RCW_FLIPVH;
break;
default:
v = O_NONE;
break;
}
return v;
}
/* *** READ *** */
/*!
@ -145,6 +243,24 @@ public:
return true;
}
/*!
* \brief initForReadingAndRollBack
* Initialize the device for reading and rollback the device to start position.
* \param device The source device.
* \return True on success, otherwise false.
*/
bool initForReadingAndRollBack(QIODevice *device)
{
if (device) {
device->startTransaction();
}
auto ok = initForReading(device);
if (device) {
device->rollbackTransaction();
}
return ok;
}
/*!
* \brief jxrFormat
* \return The JXR format.
@ -318,11 +434,20 @@ public:
}
/*!
* \brief setTextMetadata
* Set the text metadata into \a image
* \brief exifData
* \return The EXIF data.
*/
MicroExif exifData() const
{
return m_exif;
}
/*!
* \brief setMetadata
* Set the metadata into \a image
* \param image Image on which to write metadata
*/
void setTextMetadata(QImage& image)
void setMetadata(QImage& image)
{
auto xmp = xmpData();
if (!xmp.isEmpty()) {
@ -344,10 +469,6 @@ public:
if (!model.isEmpty()) {
image.setText(QStringLiteral(META_KEY_MODEL), model);
}
auto cDate = dateTime();
if (!cDate.isEmpty()) {
image.setText(QStringLiteral(META_KEY_CREATIONDATE), cDate);
}
auto author = artist();
if (!author.isEmpty()) {
image.setText(QStringLiteral(META_KEY_AUTHOR), author);
@ -368,20 +489,23 @@ public:
if (!docn.isEmpty()) {
image.setText(QStringLiteral(META_KEY_DOCUMENTNAME), docn);
}
auto exif = exifData();
if (!exif.isEmpty()) {
exif.updateImageMetadata(image);
}
}
#define META_TEXT(name, key) \
QString name() const \
{ \
readTextMeta(); \
return txtMeta.value(QStringLiteral(key)); \
return m_txtMeta.value(QStringLiteral(key)); \
}
META_TEXT(description, META_KEY_DESCRIPTION)
META_TEXT(cameraMake, META_KEY_MANUFACTURER)
META_TEXT(cameraModel, META_KEY_MODEL)
META_TEXT(software, META_KEY_SOFTWARE)
META_TEXT(dateTime, META_KEY_CREATIONDATE)
META_TEXT(artist, META_KEY_AUTHOR)
META_TEXT(copyright, META_KEY_COPYRIGHT)
META_TEXT(caption, META_KEY_TITLE)
@ -400,9 +524,9 @@ public:
bool initForWriting()
{
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
QSharedPointer<QFile> file(new QFile(fileName));
jxrFile = file;
m_jxrFile = file;
return initEncoder();
}
@ -422,7 +546,7 @@ public:
return false;
}
if (!deviceCopy(device, jxrFile.data())) {
if (!deviceCopy(device, m_jxrFile.data())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while writing in the target device";
return false;
}
@ -545,6 +669,9 @@ public:
wmiSCP->cNumOfSliceMinus1H = wmiSCP->cNumOfSliceMinus1V = 0;
wmiSCP->sbSubband = SB_ALL;
wmiSCP->uAlphaMode = image.hasAlphaChannel() ? 2 : 0;
if (quality() > -1) {
wmiSCP->uiDefaultQPIndex = qBound(0, 100 - quality(), 100);
}
return true;
}
@ -580,7 +707,6 @@ public:
META_CTEXT(META_KEY_MODEL, pvarCameraModel)
META_CTEXT(META_KEY_AUTHOR, pvarArtist)
META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright)
META_CTEXT(META_KEY_CREATIONDATE, pvarDateTime)
META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
META_WTEXT(META_KEY_TITLE, pvarCaption)
@ -595,12 +721,33 @@ public:
meta.pvarSoftware.VT.pszVal = software.data();
}
// Date and Time (TIFF format)
auto cDate = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
auto sDate = cDate.isValid() ? cDate.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toLatin1() : QByteArray();
if (!sDate.isEmpty()) {
meta.pvarDateTime.vt = DPKVT_LPSTR;
meta.pvarDateTime.VT.pszVal = sDate.data();
}
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
if (!xmp.isNull()) {
if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<quint8 *>(xmp.data()), xmp.size())) {
if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(xmp.constData()), xmp.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err;
}
}
auto exif = MicroExif::fromImage(image);
if (!exif.isEmpty()) {
auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian, MicroExif::V2);
if (auto err = PKImageEncode_SetEXIFMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(exifIfd.constData()), exifIfd.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting EXIF data:" << err;
}
auto gpsIfd = exif.gpsIfdByteArray(QDataStream::LittleEndian, MicroExif::V2);
if (auto err = PKImageEncode_SetGPSInfoMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(gpsIfd.constData()), gpsIfd.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting GPS data:" << err;
}
}
if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err;
}
@ -710,11 +857,11 @@ private:
if (device == nullptr) {
return false;
}
if (!jxrFile.isNull()) {
if (!m_jxrFile.isNull()) {
return true;
}
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
QSharedPointer<QFile> file(new QFile(fileName));
if (!file->open(QFile::WriteOnly)) {
return false;
@ -724,7 +871,8 @@ private:
return false;
}
file->close();
jxrFile = file;
m_exif = MicroExif::fromDevice(file.data());
m_jxrFile = file;
return true;
}
@ -740,6 +888,8 @@ private:
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initDecoder() unable to create decoder:" << err;
return false;
}
setTransformation(JXRHandlerPrivate::orientationToTransformation(pDecoder->WMP.wmiI.oOrientation));
pDecoder->WMP.wmiI.oOrientation = O_NONE; // disable the library rotation application
return true;
}
@ -755,6 +905,7 @@ private:
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initEncoder() unable to create encoder:" << err;
return false;
}
pEncoder->WMP.oOrientation = JXRHandlerPrivate::transformationToOrientation(transformation());
return true;
}
@ -762,7 +913,7 @@ private:
if (pDecoder == nullptr) {
return false;
}
if (!txtMeta.isEmpty()) {
if (!m_txtMeta.isEmpty()) {
return true;
}
@ -773,15 +924,14 @@ private:
#define META_TEXT(name, field) \
if (meta.field.vt == DPKVT_LPSTR) \
txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \
m_txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \
else if (meta.field.vt == DPKVT_LPWSTR) \
txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));
m_txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));
META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription)
META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake)
META_TEXT(META_KEY_MODEL, pvarCameraModel)
META_TEXT(META_KEY_SOFTWARE, pvarSoftware)
META_TEXT(META_KEY_CREATIONDATE, pvarDateTime)
META_TEXT(META_KEY_AUTHOR, pvarArtist)
META_TEXT(META_KEY_COPYRIGHT, pvarCopyright)
META_TEXT(META_KEY_TITLE, pvarCaption)
@ -859,7 +1009,7 @@ bool JXRHandler::read(QImage *outImage)
return false;
}
for (qint32 y = 0, h = img.height(); y < h; ++y) {
std::memcpy(img.scanLine(y), ba.data() + convStrideSize * y, (std::min)(convStrideSize, img.bytesPerLine()));
std::memcpy(img.scanLine(y), ba.data() + convStrideSize * y, (std::min)(convStrideSize, qint64(img.bytesPerLine())));
}
}
PKFormatConverter_Release(&pConverter);
@ -867,7 +1017,7 @@ bool JXRHandler::read(QImage *outImage)
// Metadata (e.g.: icc profile, description, etc...)
img.setColorSpace(d->colorSpace());
d->setTextMetadata(img);
d->setMetadata(img);
#ifndef JXR_DENY_FLOAT_IMAGE
// JXR float are stored in scRGB.
@ -921,6 +1071,10 @@ bool JXRHandler::write(const QImage &image)
return false;
}
#ifndef JXR_DISABLE_BGRA_HACK
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGB)) {
jxlfmt = GUID_PKPixelFormat32bppBGR;
qi.rgbSwap();
}
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGBA)) {
jxlfmt = GUID_PKPixelFormat32bppBGRA;
qi.rgbSwap();
@ -937,10 +1091,6 @@ bool JXRHandler::write(const QImage &image)
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating encoder parameters for" << qi.format();
return false;
}
if (m_quality > -1) {
wmiSCP.uiDefaultQPIndex = qBound(0, 100 - m_quality, 100);
}
if (auto err = d->pEncoder->Initialize(d->pEncoder, pEncodeStream, &wmiSCP, sizeof(wmiSCP))) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while initializing the encoder:" << err;
return false;
@ -983,10 +1133,18 @@ bool JXRHandler::write(const QImage &image)
void JXRHandler::setOption(ImageOption option, const QVariant &value)
{
if (option == QImageIOHandler::Quality) {
bool ok = false;
auto ok = false;
auto q = value.toInt(&ok);
if (ok) {
m_quality = q;
d->setQuality(q);
}
}
if (option == QImageIOHandler::ImageTransformation) {
auto ok = false;
auto t = value.toInt(&ok);
if (ok) {
d->setTransformation(QImageIOHandler::Transformation(t));
}
}
}
@ -1003,7 +1161,7 @@ bool JXRHandler::supportsOption(ImageOption option) const
return true;
}
if (option == QImageIOHandler::ImageTransformation) {
return false; // disabled because test cases are missing
return true;
}
return false;
}
@ -1013,7 +1171,7 @@ QVariant JXRHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (d->initForReading(device())) {
if (d->initForReadingAndRollBack(device())) {
auto size = d->imageSize();
if (size.isValid()) {
v = QVariant::fromValue(size);
@ -1022,45 +1180,19 @@ QVariant JXRHandler::option(ImageOption option) const
}
if (option == QImageIOHandler::ImageFormat) {
if (d->initForReading(device())) {
if (d->initForReadingAndRollBack(device())) {
v = QVariant::fromValue(d->imageFormat());
}
}
if (option == QImageIOHandler::Quality) {
v = m_quality;
v = d->quality();
}
if (option == QImageIOHandler::ImageTransformation) {
// TODO: rotation info (test case needed)
if (d->initForReading(device())) {
switch (d->pDecoder->WMP.oOrientationFromContainer) {
case O_FLIPV:
v = int(QImageIOHandler::TransformationFlip);
break;
case O_FLIPH:
v = int(QImageIOHandler::TransformationMirror);
break;
case O_FLIPVH:
v = int(QImageIOHandler::TransformationRotate180);
break;
case O_RCW:
v = int(QImageIOHandler::TransformationRotate90);
break;
case O_RCW_FLIPV:
v = int(QImageIOHandler::TransformationFlipAndRotate90);
break;
case O_RCW_FLIPH:
v = int(QImageIOHandler::TransformationMirrorAndRotate90);
break;
case O_RCW_FLIPVH:
v = int(QImageIOHandler::TransformationRotate270);
break;
default:
v = int(QImageIOHandler::TransformationNone);
break;
}
}
// ignore result: I might want to read the value set in writing
d->initForReadingAndRollBack(device());
v = int(d->transformation());
}
return v;
@ -1068,7 +1200,6 @@ QVariant JXRHandler::option(ImageOption option) const
JXRHandler::JXRHandler()
: d(new JXRHandlerPrivate)
, m_quality(-1)
{
}

View File

@ -30,8 +30,6 @@ public:
private:
mutable QSharedDataPointer<JXRHandlerPrivate> d;
qint32 m_quality;
};
class JXRPlugin : public QImageIOPlugin

View File

@ -12,6 +12,7 @@
#include <QCoreApplication>
#include <QDataStream>
#include <QHash>
#include <QStringDecoder>
#include <QTimeZone>
// TIFF 6 specs
@ -37,7 +38,12 @@
// EXIF 3 specs
#define EXIF_EXIFIFD 0x8769
#define EXIF_GPSIFD 0x8825
#define EXIF_EXIFVERSION 0x9000
#define EXIF_DATETIMEORIGINAL 0x9003
#define EXIF_DATETIMEDIGITIZED 0x9004
#define EXIF_OFFSETTIME 0x9010
#define EXIF_OFFSETTIMEORIGINAL 0x9011
#define EXIF_OFFSETTIMEDIGITIZED 0x9012
#define EXIF_COLORSPACE 0xA001
#define EXIF_PIXELXDIM 0xA002
#define EXIF_PIXELYDIM 0xA003
@ -47,7 +53,6 @@
#define EXIF_LENSMODEL 0xA434
#define EXIF_LENSSERIALNUMBER 0xA435
#define EXIF_IMAGETITLE 0xA436
#define EXIF_EXIFVERSION 0x9000
#define EXIF_VAL_COLORSPACE_SRGB 1
#define EXIF_VAL_COLORSPACE_UNCAL 0xFFFF
@ -59,7 +64,8 @@
#define GPS_LONGITUDE 4
#define GPS_ALTITUDEREF 5
#define GPS_ALTITUDE 6
#define GPS_IMGDIRECTIONREF 16
#define GPS_IMGDIRECTION 17
#define EXIF_TAG_VALUE(n, byteSize) (((n) << 6) | ((byteSize) & 0x3F))
#define EXIF_TAG_SIZEOF(dataType) (quint16(dataType) & 0x3F)
#define EXIF_TAG_DATATYPE(dataType) (quint16(dataType) >> 6)
@ -106,29 +112,33 @@ static const KnownTags staticTagTypes = {
TagInfo(TIFF_IMAGEWIDTH, ExifTagType::Long),
TagInfo(TIFF_IMAGEHEIGHT, ExifTagType::Long),
TagInfo(TIFF_BITSPERSAMPLE, ExifTagType::Short),
TagInfo(TIFF_IMAGEDESCRIPTION, ExifTagType::Ascii),
TagInfo(TIFF_MAKE, ExifTagType::Ascii),
TagInfo(TIFF_MODEL, ExifTagType::Ascii),
TagInfo(TIFF_IMAGEDESCRIPTION, ExifTagType::Utf8),
TagInfo(TIFF_MAKE, ExifTagType::Utf8),
TagInfo(TIFF_MODEL, ExifTagType::Utf8),
TagInfo(TIFF_ORIENT, ExifTagType::Short),
TagInfo(TIFF_XRES, ExifTagType::Rational),
TagInfo(TIFF_YRES, ExifTagType::Rational),
TagInfo(TIFF_URES, ExifTagType::Short),
TagInfo(TIFF_SOFTWARE, ExifTagType::Ascii),
TagInfo(TIFF_ARTIST, ExifTagType::Ascii),
TagInfo(TIFF_SOFTWARE, ExifTagType::Utf8),
TagInfo(TIFF_ARTIST, ExifTagType::Utf8),
TagInfo(TIFF_DATETIME, ExifTagType::Ascii),
TagInfo(TIFF_COPYRIGHT, ExifTagType::Ascii),
TagInfo(TIFF_COPYRIGHT, ExifTagType::Utf8),
TagInfo(EXIF_EXIFIFD, ExifTagType::Long),
TagInfo(EXIF_GPSIFD, ExifTagType::Long),
TagInfo(EXIF_DATETIMEORIGINAL, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIME, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIMEORIGINAL, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii),
TagInfo(EXIF_COLORSPACE, ExifTagType::Short),
TagInfo(EXIF_PIXELXDIM, ExifTagType::Long),
TagInfo(EXIF_PIXELYDIM, ExifTagType::Long),
TagInfo(EXIF_IMAGEUNIQUEID, ExifTagType::Ascii),
TagInfo(EXIF_BODYSERIALNUMBER, ExifTagType::Ascii),
TagInfo(EXIF_LENSMAKE, ExifTagType::Ascii),
TagInfo(EXIF_LENSMODEL, ExifTagType::Ascii),
TagInfo(EXIF_LENSMAKE, ExifTagType::Utf8),
TagInfo(EXIF_LENSMODEL, ExifTagType::Utf8),
TagInfo(EXIF_LENSSERIALNUMBER, ExifTagType::Ascii),
TagInfo(EXIF_IMAGETITLE, ExifTagType::Ascii),
TagInfo(EXIF_IMAGETITLE, ExifTagType::Utf8),
TagInfo(EXIF_EXIFVERSION, ExifTagType::Undefined)
};
// clang-format on
@ -144,7 +154,9 @@ static const KnownTags staticGpsTagTypes = {
TagInfo(GPS_LONGITUDEREF, ExifTagType::Ascii),
TagInfo(GPS_LONGITUDE, ExifTagType::Rational),
TagInfo(GPS_ALTITUDEREF, ExifTagType::Byte),
TagInfo(GPS_ALTITUDE, ExifTagType::Rational)
TagInfo(GPS_ALTITUDE, ExifTagType::Rational),
TagInfo(GPS_IMGDIRECTIONREF, ExifTagType::Ascii),
TagInfo(GPS_IMGDIRECTION, ExifTagType::Rational)
};
// clang-format on
@ -230,8 +242,8 @@ static bool checkHeader(QDataStream &ds)
quint16 version;
ds >> version;
if (version != 0x2A)
return false;
if (version != 0x002A && version != 0x01BC)
return false; // not TIFF or JXR
quint32 offset;
ds >> offset;
@ -356,6 +368,28 @@ static void writeData(QDataStream &ds, const QVariant &value, const ExifTagType&
}
}
static ExifTagType updateDataType(const ExifTagType &dataType, const QVariant &value, const MicroExif::Version &ver)
{
if (dataType != ExifTagType::Utf8)
return dataType;
if (ver == MicroExif::V2)
return ExifTagType::Ascii;
// Note that in EXIF specs, UTF-8 is backward compatible with ASCII: all UTF-8 tags can also be ASCII.
// To maximize compatibility, I check if the string can be encoded in ASCII.
auto txt = value.toString();
// Exif ASCII data type allow only values up to 127 (7-bit ASCII).
auto u8 = txt.toUtf8();
for (auto &&c : u8) {
if (uchar(c) > 127)
return dataType;
}
return ExifTagType::Ascii;
}
/*!
* \brief writeIfd
* \param ds The stream.
@ -364,7 +398,12 @@ static void writeData(QDataStream &ds, const QVariant &value, const ExifTagType&
* \param knownTags List of known and supported tags.
* \return True on success, otherwise false.
*/
static bool writeIfd(QDataStream &ds, const MicroExif::Tags &tags, TagPos &positions, quint32 pos = 0, const KnownTags &knownTags = staticTagTypes)
static bool writeIfd(QDataStream &ds,
const MicroExif::Version &ver,
const MicroExif::Tags &tags,
TagPos &positions,
quint32 pos = 0,
const KnownTags &knownTags = staticTagTypes)
{
if (tags.isEmpty())
return true;
@ -379,7 +418,7 @@ static bool writeIfd(QDataStream &ds, const MicroExif::Tags &tags, TagPos &posit
continue;
}
auto value = tags.value(key);
auto dataType = knownTags.value(key);
auto dataType = updateDataType(knownTags.value(key), value, ver);
auto count = countBytes(dataType, value);
ds << quint16(key);
@ -401,9 +440,8 @@ static bool writeIfd(QDataStream &ds, const MicroExif::Tags &tags, TagPos &posit
if (!knownTags.contains(key)) {
continue;
}
auto value = tags.value(key);
auto dataType = knownTags.value(key);
auto dataType = updateDataType(knownTags.value(key), value, ver);
auto count = countBytes(dataType, value);
auto valueSize = count * EXIF_TAG_SIZEOF(dataType);
if (valueSize <= 4)
@ -523,8 +561,16 @@ static bool readIfd(QDataStream &ds, MicroExif::Tags &tags, quint32 pos = 0, con
if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Ascii) || dataType == EXIF_TAG_DATATYPE(ExifTagType::Utf8)) {
auto l = readBytes(ds, count, true);
if (!l.isEmpty())
tags.insert(tagId, dataType == EXIF_TAG_DATATYPE(ExifTagType::Utf8) ? QString::fromUtf8(l) : QString::fromLatin1(l));
if (!l.isEmpty()) {
// It seems that converting to Latin 1 never detects errors so, using UTF-8.
// Note that if the dataType is ASCII, by EXIF specification, it must use only the
// first 128 values so the UTF-8 conversion is correct.
auto dec = QStringDecoder(QStringDecoder::Utf8);
// QStringDecoder raise an error only after converting to QString
auto ut8 = QString(dec(l));
// If there are errors in the conversion to UTF-8, then I try with latin1 (extended ASCII)
tags.insert(tagId, dec.hasError() ? QString::fromLatin1(l) : ut8);
}
} else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Undefined)) {
auto l = readBytes(ds, count, false);
if (!l.isEmpty())
@ -852,6 +898,46 @@ void MicroExif::setDateTime(const QDateTime &dt)
setExifString(EXIF_OFFSETTIME, timeOffset(dt.offsetFromUtc() / 60));
}
QDateTime MicroExif::dateTimeOriginal() const
{
auto dt = QDateTime::fromString(exifString(EXIF_DATETIMEORIGINAL), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
auto ofTag = exifString(EXIF_OFFSETTIMEORIGINAL);
if (dt.isValid() && !ofTag.isEmpty())
dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
return(dt);
}
void MicroExif::setDateTimeOriginal(const QDateTime &dt)
{
if (!dt.isValid()) {
m_exifTags.remove(EXIF_DATETIMEORIGINAL);
m_exifTags.remove(EXIF_OFFSETTIMEORIGINAL);
return;
}
setExifString(EXIF_DATETIMEORIGINAL, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
setExifString(EXIF_OFFSETTIMEORIGINAL, timeOffset(dt.offsetFromUtc() / 60));
}
QDateTime MicroExif::dateTimeDigitized() const
{
auto dt = QDateTime::fromString(exifString(EXIF_DATETIMEDIGITIZED), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
auto ofTag = exifString(EXIF_OFFSETTIMEDIGITIZED);
if (dt.isValid() && !ofTag.isEmpty())
dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
return(dt);
}
void MicroExif::setDateTimeDigitized(const QDateTime &dt)
{
if (!dt.isValid()) {
m_exifTags.remove(EXIF_DATETIMEDIGITIZED);
m_exifTags.remove(EXIF_OFFSETTIMEDIGITIZED);
return;
}
setExifString(EXIF_DATETIMEDIGITIZED, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
setExifString(EXIF_OFFSETTIMEDIGITIZED, timeOffset(dt.offsetFromUtc() / 60));
}
QString MicroExif::title() const
{
return exifString(EXIF_IMAGETITLE);
@ -896,6 +982,10 @@ double MicroExif::latitude() const
void MicroExif::setLatitude(double degree)
{
if (qIsNaN(degree)) {
m_gpsTags.remove(GPS_LATITUDEREF);
m_gpsTags.remove(GPS_LATITUDE);
}
if (degree < -90.0 || degree > 90.0)
return; // invalid latitude
auto adeg = qAbs(degree);
@ -921,6 +1011,10 @@ double MicroExif::longitude() const
void MicroExif::setLongitude(double degree)
{
if (qIsNaN(degree)) {
m_gpsTags.remove(GPS_LONGITUDEREF);
m_gpsTags.remove(GPS_LONGITUDE);
}
if (degree < -180.0 || degree > 180.0)
return; // invalid longitude
auto adeg = qAbs(degree);
@ -935,17 +1029,45 @@ double MicroExif::altitude() const
auto ref = m_gpsTags.value(GPS_ALTITUDEREF);
if (ref.isNull())
return qQNaN();
if (!m_gpsTags.contains(GPS_ALTITUDE))
return qQNaN();
auto alt = m_gpsTags.value(GPS_ALTITUDE).toDouble();
return (ref.toInt() == 0 || ref.toInt() == 2) ? alt : -alt;
}
void MicroExif::setAltitude(double meters)
{
if (qIsNaN(meters)) {
m_gpsTags.remove(GPS_ALTITUDEREF);
m_gpsTags.remove(GPS_ALTITUDE);
}
m_gpsTags.insert(GPS_ALTITUDEREF, quint8(meters < 0 ? 1 : 0));
m_gpsTags.insert(GPS_ALTITUDE, meters);
}
QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const
double MicroExif::imageDirection(bool *isMagnetic) const
{
auto tmp = false;
if (isMagnetic == nullptr)
isMagnetic = &tmp;
if (!m_gpsTags.contains(GPS_IMGDIRECTION))
return qQNaN();
auto ref = gpsString(GPS_IMGDIRECTIONREF).toUpper();
*isMagnetic = (ref == QStringLiteral("M"));
return m_gpsTags.value(GPS_IMGDIRECTION).toDouble();
}
void MicroExif::setImageDirection(double degree, bool isMagnetic)
{
if (qIsNaN(degree)) {
m_gpsTags.remove(GPS_IMGDIRECTIONREF);
m_gpsTags.remove(GPS_IMGDIRECTION);
}
m_gpsTags.insert(GPS_IMGDIRECTIONREF, isMagnetic ? QStringLiteral("M") : QStringLiteral("T"));
m_gpsTags.insert(GPS_IMGDIRECTION, degree);
}
QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
QByteArray ba;
{
@ -956,7 +1078,51 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const
return ba;
}
bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder) const
QByteArray MicroExif::exifIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
QByteArray ba;
{
QDataStream ds(&ba, QIODevice::WriteOnly);
ds.setByteOrder(byteOrder);
auto exifTags = m_exifTags;
exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232"));
TagPos positions;
if (!writeIfd(ds, version, exifTags, positions))
return {};
}
return ba;
}
bool MicroExif::setExifIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder)
{
QDataStream ds(ba);
ds.setByteOrder(byteOrder);
return readIfd(ds, m_exifTags, 0, staticTagTypes);
}
QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
QByteArray ba;
{
QDataStream ds(&ba, QIODevice::WriteOnly);
ds.setByteOrder(byteOrder);
auto gpsTags = m_gpsTags;
gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
TagPos positions;
if (!writeIfd(ds, version, gpsTags, positions, 0, staticGpsTagTypes))
return {};
return ba;
}
}
bool MicroExif::setGpsIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder)
{
QDataStream ds(ba);
ds.setByteOrder(byteOrder);
return readIfd(ds, m_gpsTags, 0, staticGpsTagTypes);
}
bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
if (device == nullptr || device->isSequential() || isEmpty())
return false;
@ -965,14 +1131,14 @@ bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder
ds.setByteOrder(byteOrder);
if (!writeHeader(ds))
return false;
if (!writeIfds(ds))
if (!writeIfds(ds, version))
return false;
}
device->close();
return true;
}
void MicroExif::toImageMetadata(QImage &targetImage, bool replaceExisting) const
void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) const
{
// set TIFF strings
for (auto &&p : tiffStrMap) {
@ -993,8 +1159,13 @@ void MicroExif::toImageMetadata(QImage &targetImage, bool replaceExisting) const
}
// set date and time
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_CREATIONDATE)).isEmpty()) {
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_MODIFICATIONDATE)).isEmpty()) {
auto dt = dateTime();
if (dt.isValid())
targetImage.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), dt.toString(Qt::ISODate));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_CREATIONDATE)).isEmpty()) {
auto dt = dateTimeOriginal();
if (dt.isValid())
targetImage.setText(QStringLiteral(META_KEY_CREATIONDATE), dt.toString(Qt::ISODate));
}
@ -1015,15 +1186,47 @@ void MicroExif::toImageMetadata(QImage &targetImage, bool replaceExisting) const
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_LONGITUDE), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_DIRECTION)).isEmpty()) {
auto v = imageDirection();
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_DIRECTION), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
}
MicroExif MicroExif::fromByteArray(const QByteArray &ba)
void MicroExif::updateImageResolution(QImage &targetImage)
{
if (horizontalResolution() > 0)
targetImage.setDotsPerMeterX(qRound(horizontalResolution() / 25.4 * 1000));
if (verticalResolution() > 0)
targetImage.setDotsPerMeterY(qRound(verticalResolution() / 25.4 * 1000));
}
MicroExif MicroExif::fromByteArray(const QByteArray &ba, bool searchHeader)
{
auto ba0(ba);
if (searchHeader) {
auto idxLE = ba0.indexOf(QByteArray("II"));
auto idxBE = ba0.indexOf(QByteArray("MM"));
auto idx = -1;
if (idxLE > -1 && idxBE > -1)
idx = std::min(idxLE, idxBE);
else
idx = idxLE > -1 ? idxLE : idxBE;
if(idx > 0)
ba0 = ba0.mid(idx);
}
QBuffer buf;
buf.setData(ba);
buf.setData(ba0);
return fromDevice(&buf);
}
MicroExif MicroExif::fromRawData(const char *data, size_t size, bool searchHeader)
{
if (data == nullptr || size == 0)
return {};
return fromByteArray(QByteArray::fromRawData(data, size), searchHeader);
}
MicroExif MicroExif::fromDevice(QIODevice *device)
{
if (device == nullptr || device->isSequential())
@ -1088,12 +1291,18 @@ MicroExif MicroExif::fromImage(const QImage &image)
exif.setSoftware(sw.trimmed());
}
// TIFF Creation date and time
auto dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_CREATIONDATE)), Qt::ISODate);
// TIFF date and time
auto dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
if (!dt.isValid())
dt = QDateTime::currentDateTime();
exif.setDateTime(dt);
// EXIF original date and time
dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_CREATIONDATE)), Qt::ISODate);
if (!dt.isValid())
dt = QDateTime::currentDateTime();
exif.setDateTimeOriginal(dt);
// GPS Info
auto ok = false;
auto alt = image.text(QStringLiteral(META_KEY_ALTITUDE)).toDouble(&ok);
@ -1105,6 +1314,9 @@ MicroExif MicroExif::fromImage(const QImage &image)
auto lon = image.text(QStringLiteral(META_KEY_LONGITUDE)).toDouble(&ok);
if (ok)
exif.setLongitude(lon);
auto dir = image.text(QStringLiteral(META_KEY_DIRECTION)).toDouble(&ok);
if (ok)
exif.setImageDirection(dir);
return exif;
}
@ -1150,30 +1362,30 @@ bool MicroExif::writeHeader(QDataStream &ds) const
return ds.status() == QDataStream::Ok;
}
bool MicroExif::writeIfds(QDataStream &ds) const
bool MicroExif::writeIfds(QDataStream &ds, const Version &version) const
{
auto tiffTags = m_tiffTags;
auto exifTags = m_exifTags;
auto gpsTags = m_gpsTags;
updateTags(tiffTags, exifTags, gpsTags);
updateTags(tiffTags, exifTags, gpsTags, version);
TagPos positions;
if (!writeIfd(ds, tiffTags, positions))
if (!writeIfd(ds, version, tiffTags, positions))
return false;
if (!writeIfd(ds, exifTags, positions, positions.value(EXIF_EXIFIFD)))
if (!writeIfd(ds, version, exifTags, positions, positions.value(EXIF_EXIFIFD)))
return false;
if (!writeIfd(ds, gpsTags, positions, positions.value(EXIF_GPSIFD), staticGpsTagTypes))
if (!writeIfd(ds, version, gpsTags, positions, positions.value(EXIF_GPSIFD), staticGpsTagTypes))
return false;
return true;
}
void MicroExif::updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags) const
void MicroExif::updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const Version &version) const
{
if (exifTags.isEmpty()) {
tiffTags.remove(EXIF_EXIFIFD);
} else {
tiffTags.insert(EXIF_EXIFIFD, quint32());
exifTags.insert(EXIF_EXIFVERSION, QByteArray("0300"));
exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232"));
}
if (gpsTags.isEmpty()) {
tiffTags.remove(EXIF_GPSIFD);

View File

@ -31,14 +31,21 @@
*
* This class is a partial (or rather minimal) implementation and is only used
* to avoid including external libraries when only a few tags are needed.
*
* It reads/writes the main IFD only.
*/
class MicroExif
{
public:
using Tags = QMap<quint16, QVariant>;
/*!
* \brief The Version enum
* Exif specs version used when writing.
*/
enum Version {
V2, // V2.xx
V3 // V3.xx, use of UTF-8 data type (default)
};
/*!
* \brief MicroExif
* Constructs an empty class.
@ -201,6 +208,20 @@ public:
QDateTime dateTime() const;
void setDateTime(const QDateTime& dt);
/*!
* \brief dateTimeOriginal
* \return The date and time when the original image data was generated.
*/
QDateTime dateTimeOriginal() const;
void setDateTimeOriginal(const QDateTime& dt);
/*!
* \brief dateTimeDigitized
* \return The date and time when the image was stored as digital data.
*/
QDateTime dateTimeDigitized() const;
void setDateTimeDigitized(const QDateTime& dt);
/*!
* \brief title
* \return The title of the image.
@ -237,37 +258,110 @@ public:
double altitude() const;
void setAltitude(double meters);
/*!
* \brief imageDirection
* \param isMagnetic Set to true if the direction is relative to magnetic north, false if it is relative to true north. Leave nullptr if is not of interest.
* \return Floating-point number indicating the direction of the image when it was captured. The range of values is from 0.00 to 359.99 or NaN if not set.
*/
double imageDirection(bool *isMagnetic = nullptr) const;
void setImageDirection(double degree, bool isMagnetic = false);
/*!
* \brief toByteArray
* Converts the class to RAW data. The raw data contains:
* - TIFF header
* - MAIN IFD
* - EXIF IFD
* - GPS IFD
* \param byteOrder Sets the serialization byte order for EXIF data.
* \param version The EXIF specs version to use.
* \return A byte array containing the serialized data.
* \sa write
*/
QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) const;
/*!
* \brief exifIfdByteArray
* Convert the EXIF IFD only to RAW data. Useful when you want to add EXIF data to an existing TIFF container.
* \param byteOrder Sets the serialization byte order for the data.
* \param version The EXIF specs version to use.
* \return A byte array containing the serialized data.
*/
QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
QByteArray exifIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) const;
/*!
* \brief setExifIfdByteArray
* \param ba The RAW data of EXIF IFD.
* \param byteOrder Sets the serialization byte order of the data.
* \return True on success, otherwise false.
*/
bool setExifIfdByteArray(const QByteArray& ba, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER);
/*!
* \brief gpsIfdByteArray
* Convert the GPS IFD only to RAW data. Useful when you want to add GPS data to an existing TIFF container.
* \param byteOrder Sets the serialization byte order for the data.
* \param version The EXIF specs version to use.
* \return A byte array containing the serialized data.
*/
QByteArray gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) const;
/*!
* \brief setGpsIfdByteArray
* \param ba The RAW data of GPS IFD.
* \param byteOrder Sets the serialization byte order of the data.
* \return True on success, otherwise false.
*/
bool setGpsIfdByteArray(const QByteArray& ba, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER);
/*!
* \brief write
* Serialize the class on a device.
* Serialize the class on a device. The serialized data contains:
* - TIFF header
* - MAIN IFD
* - EXIF IFD
* - GPS IFD
* \param device A random access device.
* \param byteOrder Sets the serialization byte order for EXIF data.
* \param version The EXIF specs version to use.
* \return True on success, otherwise false.
* \sa toByteArray
*/
bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) const;
/*!
* \brief toImageMetadata
* Helper to set metadata in an image.
* \brief updateImageMetadata
* Helper to set EXIF metadata to the image.
* \param targetImage The image to set metadata on.
* \param replaceExisting Replaces any existing metadata.
*/
void toImageMetadata(QImage& targetImage, bool replaceExisting = false) const;
void updateImageMetadata(QImage &targetImage, bool replaceExisting = false) const;
/*!
* \brief updateImageResolution
* Helper to set the EXIF resolution to the image. Resolution is set only if valid.
* \param targetImage The image to set resolution on.
*/
void updateImageResolution(QImage &targetImage);
/*!
* \brief fromByteArray
* Creates the class from RAW EXIF data.
* \param ba Raw data containing EXIF data.
* \param searchHeader If true, the EXIF header is searched within the data. If false, the data must begin with the EXIF header.
* \return The created class (empty on error).
* \sa isEmpty
*/
static MicroExif fromByteArray(const QByteArray &ba);
static MicroExif fromByteArray(const QByteArray &ba, bool searchHeader = false);
/*!
* \brief fromRawData
* Creates the class from RAW EXIF data.
* \param data Raw data containing EXIF data.
* \param size The size of \a data.
* \param searchHeader If true, the EXIF header is searched within the data. If false, the data must begin with the EXIF header.
* \return The created class (empty on error).
* \sa isEmpty, fromByteArray
*/
static MicroExif fromRawData(const char *data, size_t size, bool searchHeader = false);
/*!
* \brief fromDevice
@ -292,8 +386,8 @@ private:
void setGpsString(quint16 tagId, const QString& s);
QString gpsString(quint16 tagId) const;
bool writeHeader(QDataStream &ds) const;
bool writeIfds(QDataStream &ds) const;
void updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags) const;
bool writeIfds(QDataStream &ds, const Version &version) const;
void updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const Version &version) const;
static void setString(Tags &tags, quint16 tagId, const QString &s);
static QString string(const Tags &tags, quint16 tagId);

View File

@ -545,7 +545,7 @@ static bool setExifData(QImage &img, const MicroExif &exif)
{
if (exif.isEmpty())
return false;
exif.toImageMetadata(img);
exif.updateImageMetadata(img);
return true;
}
@ -1504,7 +1504,18 @@ bool PSDHandler::read(QImage *image)
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
#endif
} else if (!setColorSpace(img, irs)) {
// qDebug() << "No colorspace info set!";
// Float images are used by Photoshop as linear: if no color space
// is present, a linear one should be chosen.
if (header.color_mode == CM_RGB && header.depth == 32) {
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if (header.color_mode == CM_GRAYSCALE && header.depth == 32) {
auto qs = QColorSpace(QPointF(0.3127, 0.3291), QColorSpace::TransferFunction::Linear);
qs.setDescription(QStringLiteral("Linear grayscale"));
img.setColorSpace(qs);
}
#endif
}
// XMP data

View File

@ -207,10 +207,10 @@ public:
auto v = QString::fromLatin1(pchar_t(res.data()), res.size()).toDouble(&ok);
if (ok && v > 0) {
if (m_pb._unitsOfMeasurement) { // Inches
return qRound(width() / v / 25.4 * 1000);
return qRoundOrZero(width() / v / 25.4 * 1000);
}
// Millimeters
return qRound(width() / v * 1000);
return qRoundOrZero(width() / v * 1000);
}
return 0;
}
@ -221,10 +221,10 @@ public:
auto v = QString::fromLatin1(pchar_t(res.data()), res.size()).toDouble(&ok);
if (ok && v > 0) {
if (m_pb._unitsOfMeasurement) { // Inches
return qRound(width() / v / 25.4 * 1000);
return qRoundOrZero(height() / v / 25.4 * 1000);
}
// Millimeters
return qRound(width() / v * 1000);
return qRoundOrZero(height() / v * 1000);
}
return 0;
}
@ -457,3 +457,4 @@ QImageIOHandler *ScitexPlugin::create(QIODevice *device, const QByteArray &forma
return handler;
}
#include "moc_sct_p.cpp"

View File

@ -20,10 +20,12 @@
#define META_KEY_COPYRIGHT "Copyright"
#define META_KEY_CREATIONDATE "CreationDate"
#define META_KEY_DESCRIPTION "Description"
#define META_KEY_DIRECTION "Direction"
#define META_KEY_DOCUMENTNAME "DocumentName"
#define META_KEY_HOSTCOMPUTER "HostComputer"
#define META_KEY_LATITUDE "Latitude"
#define META_KEY_LONGITUDE "Longitude"
#define META_KEY_MODIFICATIONDATE "ModificationDate"
#define META_KEY_OWNER "Owner"
#define META_KEY_SOFTWARE "Software"
#define META_KEY_TITLE "Title"
@ -59,4 +61,13 @@ inline QImage imageAlloc(qint32 width, qint32 height, const QImage::Format &form
return imageAlloc(QSize(width, height), format);
}
inline double qRoundOrZero(double d)
{
// If the value d is outside the range of int, the behavior is undefined.
if (d > std::numeric_limits<int>::max()) {
return 0;
}
return qRound(d);
}
#endif // UTIL_P_H

View File

@ -962,11 +962,7 @@ bool XCFImageFormat::loadImageProperties(QDataStream &xcf_io, XCFImage &xcf_imag
case PROP_PARASITES:
while (!property.atEnd()) {
char *tag;
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
quint32 size;
#else
qint64 size;
#endif
property.readBytes(tag, size);