Compare commits

...

15 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
13 changed files with 411 additions and 57 deletions

View File

@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.16)
set(KF_VERSION "6.13.0") # handled by release scripts
set(KF_DEP_VERSION "6.13.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.13.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})
@ -100,7 +100,7 @@ add_feature_info(LibJXR LibJXR_FOUND "required for the QImage plugin for JPEG XR
ecm_set_disabled_deprecation_versions(
QT 6.9.0
KF 6.12.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

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.

View File

@ -31,7 +31,7 @@
},
{
"key" : "Description",
"value" : "TV broadcast test image."
"value" : "テレビ放送テスト映像。(TV broadcast test image.)"
},
{
"key" : "Latitude",

View File

@ -31,7 +31,7 @@
},
{
"key" : "Description",
"value" : "TV broadcast test image."
"value" : "テレビ放送テスト映像。(TV broadcast test image.)"
},
{
"key" : "Latitude",

View File

@ -31,7 +31,7 @@
},
{
"key" : "Description",
"value" : "TV broadcast test image."
"value" : "テレビ放送テスト映像。(TV broadcast test image.)"
},
{
"key" : "Latitude",

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()

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

View File

@ -243,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.
@ -720,11 +738,11 @@ public:
auto exif = MicroExif::fromImage(image);
if (!exif.isEmpty()) {
auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian);
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);
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;
}
@ -991,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);
@ -1153,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);
@ -1162,7 +1180,7 @@ QVariant JXRHandler::option(ImageOption option) const
}
if (option == QImageIOHandler::ImageFormat) {
if (d->initForReading(device())) {
if (d->initForReadingAndRollBack(device())) {
v = QVariant::fromValue(d->imageFormat());
}
}
@ -1173,7 +1191,7 @@ QVariant JXRHandler::option(ImageOption option) const
if (option == QImageIOHandler::ImageTransformation) {
// ignore result: I might want to read the value set in writing
d->initForReading(device());
d->initForReadingAndRollBack(device());
v = int(d->transformation());
}

View File

@ -12,6 +12,7 @@
#include <QCoreApplication>
#include <QDataStream>
#include <QHash>
#include <QStringDecoder>
#include <QTimeZone>
// TIFF 6 specs
@ -111,17 +112,17 @@ 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),
@ -134,10 +135,10 @@ static const KnownTags staticTagTypes = {
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
@ -367,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.
@ -375,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;
@ -390,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);
@ -412,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)
@ -534,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())
@ -1032,7 +1067,7 @@ void MicroExif::setImageDirection(double degree, bool isMagnetic)
m_gpsTags.insert(GPS_IMGDIRECTION, degree);
}
QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const
QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
QByteArray ba;
{
@ -1043,16 +1078,16 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const
return ba;
}
QByteArray MicroExif::exifIfdByteArray(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, QByteArray("0300"));
exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232"));
TagPos positions;
if (!writeIfd(ds, exifTags, positions))
if (!writeIfd(ds, version, exifTags, positions))
return {};
}
return ba;
@ -1065,7 +1100,7 @@ bool MicroExif::setExifIfdByteArray(const QByteArray &ba, const QDataStream::Byt
return readIfd(ds, m_exifTags, 0, staticTagTypes);
}
QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder) const
QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
QByteArray ba;
{
@ -1074,7 +1109,7 @@ QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder) c
auto gpsTags = m_gpsTags;
gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
TagPos positions;
if (!writeIfd(ds, gpsTags, positions, 0, staticGpsTagTypes))
if (!writeIfd(ds, version, gpsTags, positions, 0, staticGpsTagTypes))
return {};
return ba;
}
@ -1087,7 +1122,7 @@ bool MicroExif::setGpsIfdByteArray(const QByteArray &ba, const QDataStream::Byte
return readIfd(ds, m_gpsTags, 0, staticGpsTagTypes);
}
bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder) const
bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
if (device == nullptr || device->isSequential() || isEmpty())
return false;
@ -1096,7 +1131,7 @@ 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();
@ -1327,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

@ -37,6 +37,15 @@ 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.
@ -265,18 +274,20 @@ public:
* - 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;
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 exifIfdByteArray(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.
@ -289,9 +300,10 @@ public:
* \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;
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.
@ -309,10 +321,11 @@ public:
* - 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 updateImageMetadata
@ -373,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

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