Compare commits

...

34 Commits

Author SHA1 Message Date
6f588c6fd3 Add missing include mocs 2025-04-03 07:34:17 +02:00
a182478e2c It compiles fine without qt6.9 deprecated methods 2025-03-23 22:52:05 +00:00
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
bef2b9168f It compiles fine without kf6.12 deprecated methods 2025-03-22 06:47:20 +01:00
473f5d9847 Write tests for hej2 format 2025-03-16 21:31:46 +01:00
9bee29cc01 heif: enable saving of hej2 format 2025-03-16 18:37:48 +01:00
cdf3be3af1 Update dependency version to 6.13.0 2025-03-14 21:31:42 +01:00
752b18a42c CI: Enable heif so we make sure it compiles 2025-03-12 16:17:29 +00:00
97a1ea181c writetest: special handling for HEIF format 2025-03-12 16:41:39 +01:00
64a43fb04f readtest: special handling for HEIF format 2025-03-12 16:17:59 +01:00
6821c29819 heif: disable AVCI decoder for libheif before 1.19.6 2025-03-12 13:26:42 +01:00
e4d95c03fa SKIP tests when libheif configuration is incomplete 2025-03-10 22:01:14 +01:00
afa8ed1a5d heif: enable reading images with native 16 bit depth 2025-03-10 19:42:34 +01:00
245c835d92 Use of heif_context_add_XMP_metadata instead
heif_context_add_XMP_metadata2
2025-03-09 10:44:18 +01:00
b2663d2651 Update dependency version to 6.13.0 2025-03-07 19:00:23 +01:00
35ab37c628 Update dependency version to 6.12.0 2025-03-07 15:09:18 +01:00
b28baa4a1e sct: qRound with param bigger than max int is undefined
oss-fuzz/399667098
2025-03-04 06:33:52 +00:00
5d2540c135 sct: Use height instead of width when calculating dotsPerMeterY 2025-03-04 06:29:58 +00:00
25cc8bc262 MicroExif: search for the TIFF signature 2025-03-03 09:17:56 +01:00
7742537f8c MicroExif: API improvements and minor bugfixes 2025-03-02 13:34:02 +00:00
d3386bbf50 Fix compilation error 2025-03-02 13:49:25 +01:00
e77986c7e0 Added support for resolution and EXIF/XMP metadata to HEIF 2025-03-02 08:03:28 +00:00
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
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
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
bb1c6aab9e Added pixel limit detected by experimental tests 2025-02-17 08:54:49 +01:00
74a734efed jxl: fix build with Qt before 6.8.0 2025-02-15 16:16:10 +01:00
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
36a6ef7d78 heif: improve handling of grayscale ICC profiles 2025-02-10 16:59:49 +00:00
9fd6896cec Improve printing details when writetest fails 2025-02-10 16:59:49 +00:00
9b14e752db Update HEIF writetest templates 2025-02-10 16:59:49 +00:00
90a2e3b412 GIT_SILENT: it compiles fine without kf6.11 deprecated methods 2025-02-10 06:46:22 +01:00
b28cf4c352 Added JXL to CMYK formats 2025-02-07 15:55:23 +01:00
b536ec4a5e Update version to 6.12.0 2025-02-07 15:38:22 +01:00
95 changed files with 1320 additions and 144 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.13.0") # handled by release scripts
set(KF_DEP_VERSION "6.13.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.11.0 NO_MODULE)
find_package(ECM 6.13.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)
@ -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.12.0
)
add_subdirectory(src)

View File

@ -125,9 +125,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 +139,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 +195,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 +220,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 +265,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 +321,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
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

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

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

@ -25,7 +25,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 +68,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 +140,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

@ -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 *** */
/*!
@ -318,11 +416,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 +451,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 +471,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 +506,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 +528,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 +651,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 +689,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 +703,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);
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);
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 +839,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 +853,8 @@ private:
return false;
}
file->close();
jxrFile = file;
m_exif = MicroExif::fromDevice(file.data());
m_jxrFile = file;
return true;
}
@ -740,6 +870,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 +887,7 @@ private:
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initEncoder() unable to create encoder:" << err;
return false;
}
pEncoder->WMP.oOrientation = JXRHandlerPrivate::transformationToOrientation(transformation());
return true;
}
@ -762,7 +895,7 @@ private:
if (pDecoder == nullptr) {
return false;
}
if (!txtMeta.isEmpty()) {
if (!m_txtMeta.isEmpty()) {
return true;
}
@ -773,15 +906,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)
@ -867,7 +999,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 +1053,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 +1073,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 +1115,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 +1143,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;
}
@ -1028,39 +1168,13 @@ QVariant JXRHandler::option(ImageOption option) const
}
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->initForReading(device());
v = int(d->transformation());
}
return v;
@ -1068,7 +1182,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

@ -37,7 +37,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 +52,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 +63,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)
@ -119,7 +124,11 @@ static const KnownTags staticTagTypes = {
TagInfo(TIFF_COPYRIGHT, ExifTagType::Ascii),
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),
@ -144,7 +153,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 +241,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;
@ -852,6 +863,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 +947,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 +976,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,16 +994,44 @@ 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);
}
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
{
QByteArray ba;
@ -956,6 +1043,50 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const
return ba;
}
QByteArray MicroExif::exifIfdByteArray(const QDataStream::ByteOrder &byteOrder) const
{
QByteArray ba;
{
QDataStream ds(&ba, QIODevice::WriteOnly);
ds.setByteOrder(byteOrder);
auto exifTags = m_exifTags;
exifTags.insert(EXIF_EXIFVERSION, QByteArray("0300"));
TagPos positions;
if (!writeIfd(ds, 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
{
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, 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
{
if (device == nullptr || device->isSequential() || isEmpty())
@ -972,7 +1103,7 @@ bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder
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 +1124,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 +1151,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 +1256,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 +1279,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;
}

View File

@ -31,8 +31,6 @@
*
* 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
{
@ -201,6 +199,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 +249,106 @@ 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.
* \return A byte array containing the serialized data.
* \sa write
*/
QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) 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.
* \return A byte array containing the serialized data.
*/
QByteArray exifIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) 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.
* \return A byte array containing the serialized data.
*/
QByteArray gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) 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.
* \return True on success, otherwise false.
* \sa toByteArray
*/
bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) 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

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