Compare commits

..

35 Commits

Author SHA1 Message Date
Nicolas Fella
84f28e8bc9 Update dependency version to 6.26.0 2026-05-01 13:20:34 +02:00
Mirco Miranda
a936927ec1 JXR: check all library return codes 2026-04-26 06:22:30 +02:00
Mirco Miranda
191e5e6a69 EXIF improvements and bugfixes
- Fixes a writing issue for float values ​​less than 1
- Fixes a missing definition of `EXIF_DATETIMEDIGITIZED` tag
- Adds support for some common camera shot metadata
- Adds missing metadata to writing tests

The following plugins automatically gain support for the new metadata: AVIF, IFF, HEIF, JXL, JXR, PSD and TGA (V2E).

The new metadata added with this patch is usually saved by smartphones (e.g. iPhone or Google Pixel).
2026-04-26 06:08:25 +02:00
Mirco Miranda
51db11eefc JXR: fix Use-of-uninitialized-value 2026-04-23 14:44:45 +02:00
Mirco Miranda
bc398382ac EXR: fix Null-dereference READ in OpenEXR 2026-04-23 11:42:16 +02:00
Mirco Miranda
d5e5012cfb HDR: fix incorrect use of s.atEnd() 2026-04-20 12:49:35 +00:00
Mirco Miranda
1b3f32a332 JXR: minimal support for multichannel 3 and 4 2026-04-20 14:13:10 +02:00
Mirco Miranda
7cf60da031 JXR: fix memory leaks 2026-04-19 20:37:27 +02:00
Mirco Miranda
d160f268e7 Improved documentation for DDS, JP2 and PSD formats 2026-04-18 08:32:32 +00:00
Mirco Miranda
276338199a EXR: fix incorrect loading of EXR files saved by Photoshop 2026 2026-04-16 15:34:33 +02:00
Mirco Miranda
742b5097f6 Fix HOST Computer metadata 2026-04-14 10:47:54 +02:00
Mirco Miranda
2d2ee68cc0 Add more info about unsecure JXR plugin 2026-04-13 12:45:09 +02:00
Nicolas Fella
d15c3c679d Update version to 6.26.0 2026-04-03 19:49:32 +02:00
Nicolas Fella
eb896efea1 Update dependency version to 6.25.0 2026-04-03 18:34:25 +02:00
Mirco Miranda
142ec14c81 TIM: PlayStation graphics read only support 2026-03-24 08:38:08 +01:00
Albert Astals Cid
38b8b70304 Remove ifdef now that we depend on Qt >= 6.9.0 2026-03-23 22:57:17 +01:00
Albert Astals Cid
e28c48cfeb GIT_SILENT Upgrade CMake version requirement to 3.29.
See https://community.kde.org/Frameworks/Policies
2026-03-23 19:30:35 +01:00
Albert Astals Cid
3c08226aec GIT_SILENT Upgrade Qt6 version requirement to 6.9.0.
See https://community.kde.org/Frameworks/Policies
2026-03-23 19:13:47 +01:00
Mirco Miranda
7c86ccaefb IFF: fix Integer-overflow in IDATChunk::strideSize 2026-03-23 12:05:01 +01:00
Nicolas Fella
ec0610d5b0 Update version to 6.25.0 2026-03-07 22:30:10 +01:00
Nicolas Fella
ae279c55f4 Update dependency version to 6.24.0 2026-03-07 21:18:31 +01:00
Mirco Miranda
836e0a53bb JP2: fix possible Undefined-shift 2026-03-04 08:15:48 +01:00
Mirco Miranda
5eb09116b0 IFF: fix buffer read overflow 2026-02-24 08:57:45 +01:00
Mirco Miranda
92368ca58f Fix Heap-buffer-overflow WRITE 2026-02-22 18:27:34 +00:00
Mirco Miranda
a91c7ef72f Fixed excessively frequent warning messages 2026-02-22 14:56:21 +01:00
Daniel Novomeský
ea2a4aafab ossfuzz: update aom, libavif, openjpeg 2026-02-19 11:31:12 +01:00
Mirco Miranda
c254875780 ANI: fix possible QByteArray allocation exception 2026-02-16 10:12:05 +01:00
Daniel Novomeský
f3de2e77c1 jxl: adjust metadata size limits
Previously there was no limit for uncompressed metadata,
but when compressed metadata required buffer larger than 4MB,
image decoding stopped.

Now the plugin discards/skips metadata boxes larger than 8MB
without stopping image decoding.
If size of compressed box is above 8MB,
we do not attempt to decompress it.
File with compressed metadata could be rejected only in rare cases
when the decompression buffer grows above 32MB (four times 8M).
2026-02-13 15:44:39 +01:00
Mirco Miranda
1ef779f370 RGB: fix a possible exception on the new 2026-02-12 13:42:58 +01:00
Laurent Montel
169a874cba GIT_SILENT: Bump kf ecm_set_disabled_deprecation_versions. Make sure that it compiles fine without kf 6.23 deprecated methods 2026-02-10 07:52:45 +01:00
Mirco Miranda
ebf77ccdf5 TGA: fix Undefined-shift 2026-02-09 23:02:06 +00:00
Mirco Miranda
359cb039d2 PSD: improve conversion sanity checks 2026-02-09 22:50:51 +00:00
Mirco Miranda
f4b91d8a54 IFF: fix compilation warnings 2026-02-09 22:46:04 +00:00
Mirco Miranda
263b5a88e2 ANI: check for array allocation size 2026-02-09 08:41:40 +01:00
Nicolas Fella
8d07f7db1b Update version to 6.24.0 2026-02-06 13:31:01 +01:00
45 changed files with 1351 additions and 150 deletions

View File

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

View File

@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.27)
cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.23.0") # handled by release scripts
set(KF_DEP_VERSION "6.23.0") # handled by release scripts
set(KF_VERSION "6.26.0") # handled by release scripts
set(KF_DEP_VERSION "6.26.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.23.0 NO_MODULE)
find_package(ECM 6.26.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.8.0)
set(REQUIRED_QT_VERSION 6.9.0)
find_package(Qt6Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(KF6Archive ${KF_DEP_VERSION})
@@ -97,16 +97,17 @@ set_package_properties(LibRaw PROPERTIES
PURPOSE "Required for the QImage plugin for RAW images"
)
# JXR plugin disabled by default due to security issues
option(KIMAGEFORMATS_JXR "Enable plugin for JPEG XR format" OFF)
if(KIMAGEFORMATS_JXR)
# JXR plugin disabled by default due to security issues.
# You should not enable it unless you know what you are doing.
option(KIMAGEFORMATS_WITH_KNOWN_CRASHES_JXR "Enable plugin for JPEG XR format" OFF)
if(KIMAGEFORMATS_WITH_KNOWN_CRASHES_JXR)
find_package(LibJXR)
endif()
add_feature_info(LibJXR LibJXR_FOUND "required for the QImage plugin for JPEG XR images")
ecm_set_disabled_deprecation_versions(
QT 6.11.0
KF 6.21.0
KF 6.23.0
)
add_subdirectory(src)

View File

@@ -21,6 +21,7 @@ The following image formats have read-only support:
- Krita (kra)
- OpenRaster (ora)
- Pixar raster (pxr)
- PlayStation graphics (tim)
- Portable FloatMap/HalfMap (pfm, phm)
- Photoshop documents (psd, psb, pdd, psdt)
- Radiance HDR (hdr)
@@ -154,8 +155,31 @@ About the image:
- `Owner`: Name of the owner of the image.
- `Software`: Name and version number of the software package(s) used to
create the image.
- `Speed`: Floating-point number indicating the speed of GPS receiver
movement in Km/h (e.g. 30.2).
- `Title`: The title of the image.
About the shot:
- `DigitalZoomRatio`: Floating-point number indicating the digital zoom ratio
when the image was shot.
- `ExposureMode`: Integer number indicating the exposure mode set when the
image was shot as reported in the EXIF specifications.
- `ExposureProgram`: Integer number indicating the class of the program used
by the camera to set exposure when the picture is taken as reported in the
EXIF specifications.
- `ExposureTime`: Floating-point number indicating the exposure time,
given in seconds (s).
- `Flash`: Integer number indicating the status of flash when the image
was shot as reported in the EXIF specifications.
- `FNumber`: Floating-point number indicating the F number.
- `FocalLength`: Floating-point number indicating the actual focal length
of the lens, in millimeters (mm).
- `ISOSpeedRatings`: Integer number indicating the sensitivity of the camera
or input device when the image was shot as reported in the EXIF
specifications.
- `WhiteBalance`: Integer number indicating the white balance mode set when
the image was shot as reported in the EXIF specifications.
About the camera:
- `Manufacturer`: The manufacturer of the recording equipment.
- `Model`: The model name or model number of the recording equipment.
@@ -250,6 +274,7 @@ limit depends on the format encoding).
- RAW: 65,535 x 65,535 pixels
- RGB: 65,535 x 65,535 pixels
- SCT: 300,000 x 300,000 pixels
- TIM: 65,535 x 65,535 pixels
- TGA: 65,535 x 65,535 pixels
- XCF: 300,000 x 300,000 pixels
@@ -293,6 +318,11 @@ plugin:
- `DDS_DISABLE_STRIDE_ALIGNMENT`: disable the stride alignment based on DDS
pitch: it is known that some writers do not set it correctly.
When writing, it is possible to set which pixel format to use by setting the
subtypes. The default is `Automatic` which chooses the most appropriate format
based on the image. For a complete list of subformats, please use the
appropriate [`QImageWriter`](https://doc.qt.io/qt-6/qimagewriter.html) APIs.
### The HEIF plugin
**This plugin is disabled by default. It can be enabled by settings
@@ -322,6 +352,10 @@ plugin:
attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable
writes it as well.
The plugin can set the following additional metadata:
- `EXRLayerName`: A string containing the name of the EXR layer used to decode
the image.
### The EPS plugin
The plugin uses `Ghostscript` to convert the raster image. When reading it
@@ -377,6 +411,11 @@ JP2 plugin has the following limitations due to the lack of support by OpenJPEG:
- Image resolution is not supported.
- To write ICC profiles you need OpenJPEG V2.5.4 or higher
When writing, it is possible to set which format to use by setting the
following subtypes:
- `JP2` (default): Save data using the JP2 container.
- `J2K`: Save only the compressed codestream.
### The JXL plugin
**The current version of the plugin limits the image size to 256 megapixels
@@ -392,7 +431,12 @@ plugin:
### The JXR plugin
**This plugin is disabled by default. It can be enabled by settings
`KIMAGEFORMATS_JXR` to `ON` in your cmake options.**
`KIMAGEFORMATS_WITH_KNOWN_CRASHES_JXR` to `ON` in your cmake options.**
> [!caution]
> The plugin disabled by default due to security issues in [jxrlib](https://github.com/4creators/jxrlib):
> the upstream jxrlib is dead and there is no "hope" they will fix the issues.
> **You should not enable it unless you know what you are doing.**
The following defines can be defined in cmake to modify the behavior of the
plugin:
@@ -441,6 +485,13 @@ plugin:
- `PSD_NATIVE_CMYK_SUPPORT_DISABLED`: disable native support for CMYK images
when compiled with Qt 6.8+
The plugin can set the following additional metadata:
- `PSDDuotoneOptions`: Byte array in hexadecimal format of color data of the
duotone specification (the format of which is not documented). From the PSD
specification: *"Other applications that read Photoshop files can treat a
duotone image as a gray image, and just preserve the contents of the duotone
information when reading and writing the file."*
### The RAW plugin
Loading RAW images always requires a conversion. To allow the user to

View File

@@ -86,6 +86,7 @@ kimageformats_read_tests(
ras
rgb
sct
tim
tga
)

View File

@@ -48,7 +48,7 @@ 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).
(normally 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

View File

@@ -180,6 +180,7 @@ HANDLER_TYPES="ANIHandler ani
RAWHandler raw
RGBHandler rgb
ScitexHandler sct
TIMHandler tim
TGAHandler tga
XCFHandler xcf"

View File

@@ -23,7 +23,7 @@
Usage:
python infra/helper.py build_image kimageformats
python infra/helper.py build_fuzzers --sanitizer undefined|address|memory kimageformats
python infra/helper.py run_fuzzer kimageformats kimgio_[ani|avif|dds|exr|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tga|xcf]_fuzzer
python infra/helper.py run_fuzzer kimageformats kimgio_[ani|avif|dds|exr|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tim|tga|xcf]_fuzzer
*/
#include <QBuffer>
@@ -52,6 +52,7 @@
#include "raw_p.h"
#include "rgb_p.h"
#include "sct_p.h"
#include "tim_p.h"
#include "tga_p.h"
#include "xcf_p.h"

View File

@@ -33,10 +33,10 @@ git clone --depth 1 -b master https://invent.kde.org/frameworks/extra-cmake-modu
git clone --depth 1 --branch=dev git://code.qt.io/qt/qtbase.git
git clone --depth 1 --branch=dev git://code.qt.io/qt/qttools.git
git clone --depth 1 -b master https://invent.kde.org/frameworks/karchive.git
git clone --depth 1 -b v3.12.0 https://aomedia.googlesource.com/aom
git clone --depth 1 -b v1.2.1 https://github.com/AOMediaCodec/libavif.git
git clone --depth 1 -b v3.13.1 https://aomedia.googlesource.com/aom
git clone --depth 1 -b v1.3.0 https://github.com/AOMediaCodec/libavif.git
git clone --depth 1 https://github.com/strukturag/libde265.git
git clone --depth 1 -b v2.5.3 https://github.com/uclouvain/openjpeg.git
git clone --depth 1 -b v2.5.4 https://github.com/uclouvain/openjpeg.git
git clone --depth 1 https://github.com/strukturag/libheif.git
git clone --depth=1 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git
git clone --depth 1 https://github.com/LibRaw/LibRaw

Binary file not shown.

View File

@@ -0,0 +1,15 @@
[
{
"fileName" : "ps2026_testcard_rgb.png",
"colorSpace" : {
"description" : "sRGB build-in (Profilo RGB lineare)",
"primaries" : "SRgb",
"transferFunction" : "Linear",
"gamma" : 1
},
"resolution" : {
"dotsPerMeterX" : 3937,
"dotsPerMeterY" : 3937
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,7 +2,7 @@
{
"fileName" : "rgb-gimp.png",
"colorSpace" : {
"description" : "",
"description" : "Embedded RGB (linear)",
"primaries" : "Custom",
"transferFunction" : "Linear",
"gamma" : 1

Binary file not shown.

View File

@@ -14,7 +14,7 @@
},
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.10"
"value" : "LIFE Pro 2.20.35"
},
{
"key" : "Altitude",
@@ -32,6 +32,10 @@
"key" : "Description",
"value" : "TV broadcast test image."
},
{
"key" : "HostComputer",
"value" : "Windows 11 Enterprise (25H2)"
},
{
"key" : "Latitude",
"value" : "44.6478"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

View File

@@ -13,14 +13,18 @@
"key" : "ModificationDate",
"value" : "2025-02-14T15:58:44+01:00"
},
{
"key" : "Software" ,
"value" : "Adobe Photoshop 26.2 (Windows)"
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Title",
"value" : "A test"
},
{
"key" : "Software",
"value" : "KImageFormats write test"
},
{
"key" : "Author",
"value" : "KDE Project"
@@ -45,6 +49,10 @@
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "LensSerialNumber",
"value" : "S/N:1234567"
},
{
"key" : "Longitude",
"value" : "10.9254"
@@ -56,6 +64,50 @@
{
"key" : "Model",
"value" : "KImageFormats"
},
{
"key" : "SerialNumber",
"value" : "S/N:7654321"
},
{
"key" : "Speed",
"value" : "13.2"
},
{
"key" : "DigitalZoomRatio",
"value" : "3.4"
},
{
"key" : "ExposureMode",
"value" : "2"
},
{
"key" : "ExposureProgram",
"value" : "6"
},
{
"key" : "ExposureTime",
"value" : "0.004"
},
{
"key" : "Flash",
"value" : "16"
},
{
"key" : "FNumber",
"value" : "1.6"
},
{
"key" : "FocalLength",
"value" : "5.96"
},
{
"key" : "ISOSpeedRatings",
"value" : "50"
},
{
"key" : "WhiteBalance",
"value" : "1"
}
],
"resolution" : {

View File

@@ -41,6 +41,10 @@
"key" : "LensModel",
"value" : "A1234"
},
{
"key" : "LensSerialNumber",
"value" : "S/N:1234567"
},
{
"key" : "Longitude",
"value" : "10.9254"
@@ -52,6 +56,50 @@
{
"key" : "Model",
"value" : "KImageFormats"
},
{
"key" : "SerialNumber",
"value" : "S/N:7654321"
},
{
"key" : "Speed",
"value" : "13.2"
},
{
"key" : "DigitalZoomRatio",
"value" : "3.4"
},
{
"key" : "ExposureMode",
"value" : "2"
},
{
"key" : "ExposureProgram",
"value" : "6"
},
{
"key" : "ExposureTime",
"value" : "0.004"
},
{
"key" : "Flash",
"value" : "16"
},
{
"key" : "FNumber",
"value" : "1.6"
},
{
"key" : "FocalLength",
"value" : "5.96"
},
{
"key" : "ISOSpeedRatings",
"value" : "50"
},
{
"key" : "WhiteBalance",
"value" : "1"
}
],
"resolution" : {

View File

@@ -62,7 +62,7 @@ void setOptionalInfo(QImage &image, const QString &suffix)
// Set metadata
auto meta = obj.value("metadata").toArray();
for (auto jv : meta) {
for (auto &&jv : meta) {
auto obj = jv.toObject();
auto key = obj.value("key").toString();
auto val = obj.value("value").toString();
@@ -106,7 +106,7 @@ bool checkOptionalInfo(QImage &image, const QString &suffix)
// Test metadata
auto meta = obj.value("metadata").toArray();
for (auto jv : meta) {
for (auto &&jv : meta) {
auto obj = jv.toObject();
auto key = obj.value("key").toString();
auto val = obj.value("value").toString();

View File

@@ -137,6 +137,10 @@ kimageformats_add_plugin(kimg_sct SOURCES sct.cpp)
##################################
kimageformats_add_plugin(kimg_tim SOURCES tim.cpp)
##################################
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp microexif.cpp scanlineconverter.cpp)
##################################

View File

@@ -5,6 +5,7 @@
*/
#include "ani_p.h"
#include "util_p.h"
#include <QImage>
#include <QLoggingCategory>
@@ -101,7 +102,7 @@ bool ANIHandler::read(QImage *outImage)
}
const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
if (!frameSize) {
if (!frameSize || frameSize > quint32(kMaxQVectorSize)) {
return false;
}
@@ -417,6 +418,9 @@ bool ANIHandler::ensureScanned() const
// IART and INAM are technically inside LIST->INFO but "INFO" is supposedly optional
// so just handle those two attributes wherever we encounter them
} else if (chunkId == "INAM" || chunkId == "IART") {
if (chunkSize > kMaxQVectorSize) {
return false;
}
const QByteArray value = device()->read(chunkSize);
if (static_cast<quint32_le>(value.size()) != chunkSize) {

View File

@@ -513,21 +513,12 @@ bool QAVIFHandler::decode_one_frame()
#else
switch (m_decoder->image->imir.axis) {
#endif
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
case 0: // top-to-bottom
result = result.mirrored(false, true);
break;
case 1: // left-to-right
result = result.mirrored(true, false);
break;
#else
case 0: // top-to-bottom
result = result.flipped(Qt::Vertical);
break;
case 1: // left-to-right
result = result.flipped(Qt::Horizontal);
break;
#endif
}
}

View File

@@ -1080,6 +1080,7 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
// (red and green modify operations are unavailable)
auto ctlbits = bitplanes > 5 ? 2 : 1;
auto max = (1 << (bitplanes - ctlbits)) - 1;
auto wrongIdx = false;
quint8 prev[3] = {};
for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
for (qint32 j = 0; j < 8; ++j, ++cnt) {
@@ -1111,7 +1112,7 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
prev[1] = qGreen(pal.at(idx));
prev[2] = qBlue(pal.at(idx));
} else {
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
wrongIdx = true;
}
break;
}
@@ -1121,6 +1122,9 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
ba[cnt3 + 2] = char(prev[2]);
}
}
if (wrongIdx) {
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): HAM palette index out of range!";
}
} else if ((modeId & CAMGChunk::ModeId::HalfBrite) && (cmap) &&
(bitplanes >= BITPLANES_HALFBRIDE_MIN && bitplanes <= BITPLANES_HALFBRIDE_MAX)) {
// From A Quick Introduction to IFF.txt:
@@ -1133,6 +1137,7 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
// absolute colors.
ba = QByteArray(rowLen * 8, char());
auto palSize = cmap->count();
auto wrongIdx = false;
for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
for (qint32 j = 0; j < 8; ++j, ++cnt) {
quint8 idx = 0, ctl = 0;
@@ -1147,10 +1152,13 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
if (idx < palSize) {
ba[cnt] = ctl ? idx + palSize : idx;
} else {
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
wrongIdx = true;
}
}
}
if (wrongIdx) {
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): HalfBrite palette index out of range!";
}
} else {
// From A Quick Introduction to IFF.txt:
//
@@ -2538,7 +2546,7 @@ static QByteArray decompressVdat(const QByteArray &comp)
static QByteArray vdatToIlbmPlane(const QByteArray &vdatData, const BMHDChunk *header)
{
QByteArray ba(vdatData.size(), char());
auto rowLen = header->rowLen();
auto rowLen = qint32(header->rowLen());
for (auto x = 0, n = 0; x < rowLen; x += 2) {
for (auto y = 0, off = x, h = header->height(); y < h; y++, off += rowLen) {
if ((off + 1 >= ba.size()) || n + 1 >= vdatData.size()) {
@@ -2844,7 +2852,7 @@ bool PLTEChunk::isValid() const
if (dataBytes() < 4) {
return false;
}
if (dataBytes() - 4 < total() * 3) {
if (dataBytes() - 4 < quint32(total()) * 3) {
return false;
}
return chunkId() == PLTEChunk::defaultChunkId();
@@ -3004,7 +3012,7 @@ QByteArray IDATChunk::strideRead(QIODevice *d, qint32 y, const IHDRChunk *header
}
if (header->model() == IHDRChunk::CLut4) {
if (rr.size() < header->width() / 2) {
if (rr.size() < (qint64(header->width()) + 1) / 2) {
return {};
}
QByteArray tmp(header->width(), char());
@@ -3084,7 +3092,8 @@ quint32 IDATChunk::strideSize(const IHDRChunk *header) const
return 0;
}
auto rs = (header->width() * header->depth() + 7) / 8;
// width() and depth() are at most 65535
auto rs = (quint32(header->width()) * header->depth() + 7) / 8;
// No padding bytes are inserted in the data.
if (header->model() == IHDRChunk::Rgb888) {

View File

@@ -58,6 +58,7 @@
#include <ImathBox.h>
#include <ImfArray.h>
#include <ImfBoxAttribute.h>
#include <ImfOpaqueAttribute.h>
#include <ImfChannelListAttribute.h>
#include <ImfCompressionAttribute.h>
#include <ImfConvert.h>
@@ -227,25 +228,57 @@ static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
/*!
* \brief viewList
* \param header
* \param header The image header.
* \return The list of available views.
* \note This plugin does not support compositing layers which are returned as single images.
*/
static QStringList viewList(const Imf::Header &h)
{
QStringList l;
if (auto views = h.findTypedAttribute<Imf::StringVectorAttribute>("multiView")) {
// Internally OpenEXR first checks if the multiView attribute is present:
// if present, I have no other layers.
for (auto &&v : views->value()) {
l << QString::fromStdString(v);
}
} else {
// Recent versions of Photoshop save images by setting the layer.
// Channels are named Layer 1.A, Layer 1.B, etc., so I have to set
// the layer or the images will appear black.
auto channels = h.channels();
for (auto i = channels.begin(); i != channels.end(); ++i) {
auto name = QString::fromLatin1(i.name(), -1);
auto idx = name.indexOf(QChar(u'.'));
if (idx > -1)
l << name.left(idx);
}
l.removeDuplicates();
}
return l;
}
static QString setLayerName(Imf::RgbaInputFile &file, qint32 imageNumber = -1)
{
// set the image to load
QString layerName;
auto &&header = file.header();
if (imageNumber > -1) {
auto views = viewList(header);
if (imageNumber < views.count())
layerName = views.at(imageNumber);
}
// set the layer name
if (!layerName.isEmpty()) {
file.setLayerName(layerName.toStdString());
}
return layerName;
}
#ifdef QT_DEBUG
static void printAttributes(const Imf::Header &h)
{
for (auto i = h.begin(); i != h.end(); ++i) {
qCDebug(LOG_EXRPLUGIN) << i.name();
qCDebug(LOG_EXRPLUGIN) << i.name() << i.attribute().typeName();
}
}
#endif
@@ -340,15 +373,29 @@ static void readColorSpace(const Imf::Header &header, QImage &image)
{
// final color operations
QColorSpace cs;
if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>("chromaticities")) {
auto &&v = chroma->value();
cs = QColorSpace(QPointF(v.white.x, v.white.y),
QPointF(v.red.x, v.red.y),
QPointF(v.green.x, v.green.y),
QPointF(v.blue.x, v.blue.y),
QColorSpace::TransferFunction::Linear);
// Photoshop 2026 allow to save the ICC profile as "iccProfile" attribute
if (auto iccProfile = header.findTypedAttribute<Imf::OpaqueAttribute>("iccProfile")) {
auto &&v = iccProfile->data();
cs = QColorSpace::fromIccProfile(QByteArray::fromRawData(v, v.size()));
}
if (!cs.isValid()) {
// Creating the ICC profile from Chromaticities
if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>("chromaticities")) {
auto &&v = chroma->value();
cs = QColorSpace(QPointF(v.white.x, v.white.y),
QPointF(v.red.x, v.red.y),
QPointF(v.green.x, v.green.y),
QPointF(v.blue.x, v.blue.y),
QColorSpace::TransferFunction::Linear);
if (cs.isValid())
cs.setDescription(QStringLiteral("Embedded RGB (linear)"));
}
}
if (!cs.isValid()) {
// Use a linear profile
cs = QColorSpace(QColorSpace::SRgbLinear);
}
image.setColorSpace(cs);
@@ -377,12 +424,7 @@ bool EXRHandler::read(QImage *outImage)
auto &&header = file.header();
// set the image to load
if (m_imageNumber > -1) {
auto views = viewList(header);
if (m_imageNumber < views.count()) {
file.setLayerName(views.at(m_imageNumber).toStdString());
}
}
auto layerName = setLayerName(file, m_imageNumber);
// get image info
Imath::Box2i dw = file.dataWindow();
@@ -401,6 +443,9 @@ bool EXRHandler::read(QImage *outImage)
qCWarning(LOG_EXRPLUGIN) << "Failed to allocate image, invalid size?" << QSize(width, height);
return false;
}
if (!layerName.isEmpty()) {
image.setText(QStringLiteral("EXRLayerName"), layerName);
}
Imf::Array2D<Imf::Rgba> pixels;
pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
@@ -688,12 +733,7 @@ QVariant EXRHandler::option(ImageOption option) const
try {
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
if (m_imageNumber > -1) { // set the image to read
auto views = viewList(file.header());
if (m_imageNumber < views.count()) {
file.setLayerName(views.at(m_imageNumber).toStdString());
}
}
setLayerName(file, m_imageNumber);
Imath::Box2i dw = file.dataWindow();
v = QVariant(QSize(dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1));
} catch (const std::exception &) {
@@ -713,6 +753,7 @@ QVariant EXRHandler::option(ImageOption option) const
try {
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
setLayerName(file, m_imageNumber);
v = QVariant::fromValue(imageFormat(file));
} catch (const std::exception &) {
// broken file or unsupported version
@@ -787,12 +828,9 @@ bool EXRHandler::canRead(QIODevice *device)
return false;
}
#if OPENEXR_VERSION_MAJOR == 3 && OPENEXR_VERSION_MINOR > 2
// openexpr >= 3.3 uses seek and tell extensively
if (device->isSequential()) {
return false;
}
#endif
const QByteArray head = device->peek(4);

View File

@@ -86,6 +86,8 @@ private:
* - 7: lossy 4-by-4 pixel block compression, fields are compressed more
* - 8: lossy DCT based compression, in blocks of 32 scanlines. More efficient for partial buffer access.
* - 9: lossy DCT based compression, in blocks of 256 scanlines. More efficient space wise and faster to decode full frames than DWAA_COMPRESSION.
* - 10: High-Throughput JPEG2000 (HTJ2K), 256 scanlines (requires OpenEXR 3.4+).
* - 11: High-Throughput JPEG2000 (HTJ2K), 32 scanlines (requires OpenEXR 3.4+).
*/
qint32 m_compressionRatio;

View File

@@ -54,10 +54,10 @@ public:
{
return width() > 0 && height() > 0 && width() <= HDR_MAX_IMAGE_WIDTH && height() <= HDR_MAX_IMAGE_HEIGHT;
}
qint32 width() const { return(m_size.width()); }
qint32 height() const { return(m_size.height()); }
QString software() const { return(m_software); }
QImageIOHandler::Transformations transformation() const { return(m_transformation); }
qint32 width() const { return m_size.width(); }
qint32 height() const { return m_size.height(); }
QString software() const { return m_software; }
QImageIOHandler::Transformations transformation() const { return m_transformation; }
/*!
* \brief colorSpace
@@ -73,7 +73,7 @@ public:
* 0.600 0.150 0.060 0.333 0.333" for red, green, blue
* and white, respectively.
*/
QColorSpace colorSpace() const { return(m_colorSpace); }
QColorSpace colorSpace() const { return m_colorSpace; }
/*!
* \brief exposure
@@ -247,7 +247,7 @@ static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
s >> image[2];
s >> image[3];
if (s.atEnd()) {
if (s.status() != QDataStream::Ok) {
return false;
}
@@ -340,20 +340,24 @@ static bool LoadHDR(QDataStream &s, const Header& h, QImage &img)
// determine scanline type
if ((width < MINELEN) || (MAXELEN < width)) {
Read_Old_Line(image, width, s);
if (!Read_Old_Line(image, width, s)) {
return false;
}
RGBE_To_QRgbLine(image, scanline, h);
continue;
}
s >> val;
if (s.atEnd()) {
return true;
if (s.status() != QDataStream::Ok) {
return false;
}
if (val != 2) {
s.device()->ungetChar(val);
Read_Old_Line(image, width, s);
if (!Read_Old_Line(image, width, s)) {
return false;
}
RGBE_To_QRgbLine(image, scanline, h);
continue;
}
@@ -362,13 +366,15 @@ static bool LoadHDR(QDataStream &s, const Header& h, QImage &img)
s >> image[2];
s >> image[3];
if (s.atEnd()) {
return true;
if (s.status() != QDataStream::Ok) {
return false;
}
if ((image[1] != 2) || (image[2] & 128)) {
image[0] = 2;
Read_Old_Line(image + 4, width - 1, s);
if (!Read_Old_Line(image + 4, width - 1, s)) {
return false;
}
RGBE_To_QRgbLine(image, scanline, h);
continue;
}
@@ -382,7 +388,7 @@ static bool LoadHDR(QDataStream &s, const Header& h, QImage &img)
for (int i = 0, len = int(lineArray.size()); i < 4; i++) {
for (int j = 0; j < width;) {
s >> code;
if (s.atEnd()) {
if (s.status() != QDataStream::Ok) {
qCDebug(HDRPLUGIN) << "Truncated HDR file";
return false;
}
@@ -510,7 +516,7 @@ bool HDRHandler::canRead(QIODevice *device)
}
// the .pic taken from official test cases does not start with this string but can be loaded.
if(device->peek(11) == "#?RADIANCE\n" || device->peek(7) == "#?RGBE\n") {
if (device->peek(11) == "#?RADIANCE\n" || device->peek(7) == "#?RGBE\n") {
return true;
}

View File

@@ -474,11 +474,7 @@ bool IFFHandler::readMayaImage(QImage *image)
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.drawImage(tp, ti);
}
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
img.mirror(false, true);
#else
img.flip(Qt::Orientation::Vertical);
#endif
addMetadata(img, form);
*image = img;

View File

@@ -253,20 +253,27 @@ public:
bool jp2ToImage(QImage *img) const
{
Q_ASSERT(img->depth() == 8 * sizeof(T) || img->depth() == 32 * sizeof(T));
for (qint32 c = 0, cc = m_jp2_image->numcomps; c < cc; ++c) {
auto cs = cc == 1 ? 1 : 4;
if (img->width() < 1 || img->height() < 1) {
return false;
}
auto maxChannels = qint32(img->bytesPerLine() / sizeof(T) / img->width());
for (qint32 c = 0, cc = std::min(qint32(m_jp2_image->numcomps), maxChannels); c < cc; ++c) {
auto cs = std::min(cc == 1 ? 1 : 4, maxChannels);
auto &&jc = m_jp2_image->comps[c];
if (jc.data == nullptr)
if (jc.data == nullptr) {
return false;
if (qint32(jc.w) != img->width() || qint32(jc.h) != img->height())
}
if (qint32(jc.w) != img->width() || qint32(jc.h) != img->height()) {
return false;
}
// discriminate between int and float (avoid complicating things by creating classes with template specializations)
if (std::numeric_limits<T>::is_integer) {
auto divisor = 1;
if (jc.prec > sizeof(T) * 8) {
auto divisor = 1ull;
auto prec = std::min(size_t(jc.prec), sizeof(*jc.data) * 8);
if (prec > sizeof(T) * 8 && prec < 64) {
// convert to the wanted precision (e.g. 16-bit -> 8-bit: divisor = 65535 / 255 = 257)
divisor = std::max(1, int(((1ll << jc.prec) - 1) / ((1ll << (sizeof(T) * 8)) - 1)));
divisor = std::max(1ull, (((1ull << prec) - 1) / ((1ull << (sizeof(T) * 8)) - 1)));
}
for (qint32 y = 0, h = img->height(); y < h; ++y) {
auto ptr = reinterpret_cast<T *>(img->scanLine(y));

View File

@@ -2008,6 +2008,11 @@ bool QJpegXLHandler::extractBox(QByteArray &output, size_t container_size)
return false;
}
if (rawboxsize > 8388608) { // 8MB limit
qCWarning(LOG_JXLPLUGIN, "Skipped decoding of big JXL metadata box");
return true;
}
output.resize(rawboxsize);
status = JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast<uint8_t *>(output.data()), output.size());
if (status != JXL_DEC_SUCCESS) {
@@ -2021,7 +2026,7 @@ bool QJpegXLHandler::extractBox(QByteArray &output, size_t container_size)
if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
size_t bytes_remains = JxlDecoderReleaseBoxBuffer(m_decoder);
if (output.size() > 4194304) { // approx. 4MB limit for decompressed metadata box
if (output.size() > 33554432) { // approx. 32MB (4*8) limit for decompressed metadata box
qCWarning(LOG_JXLPLUGIN, "JXL metadata box is too large");
m_parseState = ParseJpegXLError;
return false;

View File

@@ -35,14 +35,19 @@
#include <cstring>
Q_DECLARE_LOGGING_CATEGORY(LOG_JXRPLUGIN)
#ifdef QT_DEBUG
Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtDebugMsg)
#else
Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
#endif
/*!
* Support for float images
*
* NOTE: Float images have values greater than 1 so they need an additional in place conversion.
*/
// #define JXR_DENY_FLOAT_IMAGE
// #define JXR_DENY_FLOAT_IMAGE // default commented
/*!
* Remove the needs of additional memory by disabling the conversion between
@@ -112,28 +117,35 @@ public:
, m_transformations(QImageIOHandler::TransformationNone)
{
m_tempDir = QSharedPointer<QTemporaryDir>(new QTemporaryDir);
if (PKCreateFactory(&pFactory, PK_SDK_VERSION) == WMP_errSuccess) {
PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION);
}
if (pFactory == nullptr || pCodecFactory == nullptr) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() initialization error of JXR library!";
if (auto err = PKCreateFactory(&pFactory, PK_SDK_VERSION)) {
qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while initializing the JXR factory:" << err;
} else if (auto err = PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION)) {
qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while initializing the JXR codec factory:" << err;
}
}
JXRHandlerPrivate(const JXRHandlerPrivate &other) = default;
~JXRHandlerPrivate()
{
if (pCodecFactory) {
PKCreateCodecFactory_Release(&pCodecFactory);
}
if (pFactory) {
PKCreateFactory_Release(&pFactory);
}
if (pDecoder) {
PKImageDecode_Release(&pDecoder);
if (auto err = pDecoder->Release(&pDecoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the decoder:" << err;
}
}
if (pEncoder) {
PKImageEncode_Release(&pEncoder);
if (auto err = pEncoder->Release(&pEncoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the encoder:" << err;
}
}
if (pCodecFactory) {
if (auto err = pCodecFactory->Release(&pCodecFactory)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the codec factory:" << err;
}
}
if (pFactory) {
if (auto err = pFactory->Release(&pFactory)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the factory:" << err;
}
}
}
@@ -278,8 +290,12 @@ public:
PKPixelFormatGUID jxrFormat() const
{
PKPixelFormatGUID pixelFormatGUID = GUID_PKPixelFormatUndefined;
if (pDecoder) {
pDecoder->GetPixelFormat(pDecoder, &pixelFormatGUID);
if (pDecoder == nullptr) {
return pixelFormatGUID;
}
if (auto err = pDecoder->GetPixelFormat(pDecoder, &pixelFormatGUID)) {
qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::jxrFormat() error while getting pixel format:" << err;
return GUID_PKPixelFormatUndefined;
}
return pixelFormatGUID;
}
@@ -303,6 +319,12 @@ public:
return qtFormat;
}
// *** MCH could be RGB, CMYK ***
qtFormat = multichannelFormat(jxrfmt, colorSpace());
if (qtFormat != QImage::Format_Invalid) {
return qtFormat;
}
// *** CONVERSION WITH THE SAME DEPTH ***
// IMPORTANT: For supported conversions see JXRGluePFC.c
@@ -393,17 +415,20 @@ public:
*/
QSize imageSize() const
{
if (pDecoder) {
qint32 w, h;
pDecoder->GetSize(pDecoder, &w, &h);
if (w > JXR_MAX_IMAGE_WIDTH || h > JXR_MAX_IMAGE_HEIGHT || w < 1 || h < 1) {
qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::imageSize() Maximum image size is limited to" << JXR_MAX_IMAGE_WIDTH << "x"
<< JXR_MAX_IMAGE_HEIGHT << "pixels";
return {};
}
return QSize(w, h);
if (pDecoder == nullptr) {
return {};
}
return {};
qint32 w = 0, h = 0;
if (auto err = pDecoder->GetSize(pDecoder, &w, &h)) {
qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::imageSize() error while getting the image size:" << err;
return {};
}
if (w > JXR_MAX_IMAGE_WIDTH || h > JXR_MAX_IMAGE_HEIGHT || w < 1 || h < 1) {
qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::imageSize() Maximum image size is limited to" << JXR_MAX_IMAGE_WIDTH << "x"
<< JXR_MAX_IMAGE_HEIGHT << "pixels";
return {};
}
return QSize(w, h);
}
/*!
@@ -416,8 +441,8 @@ public:
if (pDecoder == nullptr) {
return cs;
}
quint32 size;
if (!pDecoder->GetColorContext(pDecoder, nullptr, &size) && size) {
quint32 size = 0;
if (!pDecoder->GetColorContext(pDecoder, nullptr, &size) && size > 0 && size < kMaxQVectorSize) {
QByteArray ba(size, 0);
if (!pDecoder->GetColorContext(pDecoder, reinterpret_cast<quint8 *>(ba.data()), &size)) {
cs = QColorSpace::fromIccProfile(ba);
@@ -437,7 +462,7 @@ public:
return xmp;
}
#ifdef JXR_ENABLE_ADVANCED_METADATA
quint32 size;
quint32 size = 0;
if (!PKImageDecode_GetXMPMetadata_WMP(pDecoder, nullptr, &size) && size > 0 && size < JXR_MAX_METADATA_SIZE) {
QByteArray ba(size, 0);
if (!PKImageDecode_GetXMPMetadata_WMP(pDecoder, reinterpret_cast<quint8 *>(ba.data()), &size)) {
@@ -498,7 +523,7 @@ public:
}
auto host = hostComputer();
if (!host.isEmpty()) {
image.setText(QStringLiteral(META_KEY_HOSTCOMPUTER), capt);
image.setText(QStringLiteral(META_KEY_HOSTCOMPUTER), host);
}
auto docn = documentName();
if (!docn.isEmpty()) {
@@ -556,7 +581,11 @@ public:
if (device == nullptr || pEncoder == nullptr) {
return false;
}
if (auto err = PKImageEncode_Release(&pEncoder)) {
if (auto err = pEncoder->Terminate(pEncoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while terminating the encoder:" << err;
return false;
}
if (auto err = pEncoder->Release(&pEncoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while releasing the encoder:" << err;
return false;
}
@@ -796,6 +825,47 @@ public:
return GUID_PKPixelFormatUndefined;
}
/*!
* \brief multichannelFormat
* I can only decide how to interpret multichannels by checking the color profile.
* If it's not present, I assume CMYK for 4 channels and RGB for 3 channels (like
* Windows does).
* \param jxrFormat Format to be converted.
* \param cs The color space of the image.
* \return A valid Qt format or QImage::Format_Invalid if there is no match
*/
static QImage::Format multichannelFormat(const PKPixelFormatGUID &jxrFormat, const QColorSpace& cs)
{
auto model = QColorSpace::ColorModel::Undefined;
if (cs.isValid()) {
model = cs.colorModel();
} else if (!cs.iccProfile().isEmpty()) {
model = QColorSpace::ColorModel::Gray; // means invalid
}
if (IsEqualGUID(GUID_PKPixelFormat24bpp3Channels, jxrFormat)) {
if (model == QColorSpace::ColorModel::Rgb || model == QColorSpace::ColorModel::Undefined)
return QImage::Format_RGB888;
}
if (IsEqualGUID(GUID_PKPixelFormat32bpp4Channels, jxrFormat)) {
if (model == QColorSpace::ColorModel::Cmyk || model == QColorSpace::ColorModel::Undefined)
return QImage::Format_CMYK8888;
}
if (IsEqualGUID(GUID_PKPixelFormat32bpp3ChannelsAlpha, jxrFormat)) {
if (model == QColorSpace::ColorModel::Rgb || model == QColorSpace::ColorModel::Undefined)
return QImage::Format_RGBA8888;
}
if (IsEqualGUID(GUID_PKPixelFormat64bpp3ChannelsAlpha, jxrFormat)) {
if (model == QColorSpace::ColorModel::Rgb || model == QColorSpace::ColorModel::Undefined)
return QImage::Format_RGBA64;
}
return QImage::Format_Invalid;
}
private:
static QList<std::pair<QImage::Format, PKPixelFormatGUID>> exactMatchingFormats()
{
@@ -967,7 +1037,7 @@ bool JXRHandler::read(QImage *outImage)
}
// resolution
float hres, vres;
float hres = 0, vres = 0;
if (auto err = d->pDecoder->GetResolution(d->pDecoder, &hres, &vres)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while reading resolution:" << err;
} else {
@@ -999,14 +1069,18 @@ bool JXRHandler::read(QImage *outImage)
return false;
}
if (auto err = pConverter->Initialize(pConverter, d->pDecoder, nullptr, convFmt)) {
PKFormatConverter_Release(&pConverter);
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to initialize the converter:" << err;
if (auto err = pConverter->Release(&pConverter)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
}
return false;
}
if (d->pDecoder->WMP.wmiI.cBitsPerUnit == size_t(img.depth())) { // in place conversion
if (auto err = pConverter->Copy(pConverter, &rect, img.bits(), img.bytesPerLine())) {
PKFormatConverter_Release(&pConverter);
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
if (auto err = pConverter->Release(&pConverter)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
}
return false;
}
} else { // additional buffer needed
@@ -1015,19 +1089,26 @@ bool JXRHandler::read(QImage *outImage)
qint64 limit = QImageReader::allocationLimit();
if (limit && (buffSize + img.sizeInBytes()) > limit * 1024 * 1024) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to covert due to allocation limit set:" << limit << "MiB";
if (auto err = pConverter->Release(&pConverter)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
}
return false;
}
QVector<quint8> ba(buffSize);
if (auto err = pConverter->Copy(pConverter, &rect, ba.data(), convStrideSize)) {
PKFormatConverter_Release(&pConverter);
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
if (auto err = pConverter->Release(&pConverter)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
}
return false;
}
for (qint32 y = 0, h = img.height(); y < h; ++y) {
std::memcpy(img.scanLine(y), ba.data() + convStrideSize * y, (std::min)(convStrideSize, qint64(img.bytesPerLine())));
}
}
PKFormatConverter_Release(&pConverter);
if (auto err = pConverter->Release(&pConverter)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
}
}
// Metadata (e.g.: icc profile, description, etc...)

View File

@@ -36,17 +36,26 @@
#define TIFF_VAL_URES_CENTIMETER 3
// EXIF 3 specs
#define EXIF_EXPOSURETIME 0x829A
#define EXIF_FNUMBER 0x829D
#define EXIF_EXIFIFD 0x8769
#define EXIF_EXPOSUREPROGRAM 0x8822
#define EXIF_GPSIFD 0x8825
#define EXIF_ISOSPEEDRATINGS 0x8827
#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_FLASH 0x9209
#define EXIF_FOCALLENGTH 0x920A
#define EXIF_COLORSPACE 0xA001
#define EXIF_PIXELXDIM 0xA002
#define EXIF_PIXELYDIM 0xA003
#define EXIF_EXPOSUREMODE 0xA402
#define EXIF_WHITEBALANCE 0xA403
#define EXIF_DIGITALZOOMRATIO 0xA404
#define EXIF_IMAGEUNIQUEID 0xA420
#define EXIF_BODYSERIALNUMBER 0xA431
#define EXIF_LENSMAKE 0xA433
@@ -64,6 +73,8 @@
#define GPS_LONGITUDE 4
#define GPS_ALTITUDEREF 5
#define GPS_ALTITUDE 6
#define GPS_SPEEDREF 12
#define GPS_SPEED 13
#define GPS_IMGDIRECTIONREF 16
#define GPS_IMGDIRECTION 17
#define EXIF_TAG_VALUE(n, byteSize) (((n) << 6) | ((byteSize) & 0x3F))
@@ -123,16 +134,25 @@ static const KnownTags staticTagTypes = {
TagInfo(TIFF_ARTIST, ExifTagType::Utf8),
TagInfo(TIFF_DATETIME, ExifTagType::Ascii),
TagInfo(TIFF_COPYRIGHT, ExifTagType::Utf8),
TagInfo(EXIF_EXPOSURETIME, ExifTagType::Rational),
TagInfo(EXIF_FNUMBER, ExifTagType::Rational),
TagInfo(EXIF_EXIFIFD, ExifTagType::Long),
TagInfo(EXIF_EXPOSUREPROGRAM, ExifTagType::Short),
TagInfo(EXIF_GPSIFD, ExifTagType::Long),
TagInfo(EXIF_ISOSPEEDRATINGS, ExifTagType::Short),
TagInfo(EXIF_DATETIMEORIGINAL, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii),
TagInfo(EXIF_DATETIMEDIGITIZED, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIME, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIMEORIGINAL, ExifTagType::Ascii),
TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii),
TagInfo(EXIF_FLASH, ExifTagType::Short),
TagInfo(EXIF_FOCALLENGTH, ExifTagType::Rational),
TagInfo(EXIF_COLORSPACE, ExifTagType::Short),
TagInfo(EXIF_PIXELXDIM, ExifTagType::Long),
TagInfo(EXIF_PIXELYDIM, ExifTagType::Long),
TagInfo(EXIF_EXPOSUREMODE, ExifTagType::Short),
TagInfo(EXIF_WHITEBALANCE, ExifTagType::Short),
TagInfo(EXIF_DIGITALZOOMRATIO, ExifTagType::Rational),
TagInfo(EXIF_IMAGEUNIQUEID, ExifTagType::Ascii),
TagInfo(EXIF_BODYSERIALNUMBER, ExifTagType::Ascii),
TagInfo(EXIF_LENSMAKE, ExifTagType::Utf8),
@@ -155,6 +175,8 @@ static const KnownTags staticGpsTagTypes = {
TagInfo(GPS_LONGITUDE, ExifTagType::Rational),
TagInfo(GPS_ALTITUDEREF, ExifTagType::Byte),
TagInfo(GPS_ALTITUDE, ExifTagType::Rational),
TagInfo(GPS_SPEEDREF, ExifTagType::Ascii),
TagInfo(GPS_SPEED, ExifTagType::Rational),
TagInfo(GPS_IMGDIRECTIONREF, ExifTagType::Ascii),
TagInfo(GPS_IMGDIRECTION, ExifTagType::Rational)
};
@@ -317,7 +339,7 @@ static void writeList(QDataStream &ds, const QVariant &value)
inline qint32 rationalPrecision(double v)
{
v = qAbs(v);
return 8 - qBound(0, v < 1 ? 8 : int(std::log10(v)), 8);
return v < 1 ? 8 : 8 - qBound(0, int(std::log10(v)), 8);
}
template<class T>
@@ -884,7 +906,7 @@ QDateTime MicroExif::dateTime() const
auto ofTag = exifString(EXIF_OFFSETTIME);
if (dt.isValid() && !ofTag.isEmpty())
dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
return(dt);
return dt;
}
void MicroExif::setDateTime(const QDateTime &dt)
@@ -904,7 +926,7 @@ QDateTime MicroExif::dateTimeOriginal() const
auto ofTag = exifString(EXIF_OFFSETTIMEORIGINAL);
if (dt.isValid() && !ofTag.isEmpty())
dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
return(dt);
return dt;
}
void MicroExif::setDateTimeOriginal(const QDateTime &dt)
@@ -924,7 +946,7 @@ QDateTime MicroExif::dateTimeDigitized() const
auto ofTag = exifString(EXIF_OFFSETTIMEDIGITIZED);
if (dt.isValid() && !ofTag.isEmpty())
dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
return(dt);
return dt;
}
void MicroExif::setDateTimeDigitized(const QDateTime &dt)
@@ -966,6 +988,138 @@ void MicroExif::setUniqueId(const QUuid &uuid)
setExifString(EXIF_IMAGEUNIQUEID, uuid.toString(QUuid::WithoutBraces).replace(QStringLiteral("-"), QString()));
}
double MicroExif::digitalZoomRatio() const
{
if (!m_exifTags.contains(EXIF_DIGITALZOOMRATIO))
return qQNaN();
return m_exifTags.value(EXIF_DIGITALZOOMRATIO).toDouble();
}
void MicroExif::setDigitalZoomRatio(double zoom)
{
if (qIsNaN(zoom))
m_exifTags.remove(EXIF_DIGITALZOOMRATIO);
else
m_exifTags.insert(EXIF_DIGITALZOOMRATIO, zoom);
}
quint16 MicroExif::isoSpeedRatings() const
{
return quint16(m_exifTags.value(EXIF_ISOSPEEDRATINGS).toUInt());
}
void MicroExif::setIsoSpeedRatings(quint16 iso)
{
if (iso == 0)
m_exifTags.remove(EXIF_ISOSPEEDRATINGS);
else
m_exifTags.insert(EXIF_ISOSPEEDRATINGS, iso);
}
ExposureMode MicroExif::exposureMode() const
{
auto ok = false;
auto v = m_exifTags.value(EXIF_EXPOSUREMODE).toUInt(&ok);
return ok ? ExposureMode(v) : ExposureMode::NotSet;
}
void MicroExif::setExposureMode(const ExposureMode &em)
{
if (em == ExposureMode::NotSet)
m_exifTags.remove(EXIF_EXPOSUREMODE);
else
m_exifTags.insert(EXIF_EXPOSUREMODE, quint16(em));
}
ExposureProgram MicroExif::exposureProgram() const
{
auto ok = false;
auto v = m_exifTags.value(EXIF_EXPOSUREPROGRAM).toUInt(&ok);
return ok ? ExposureProgram(v) : ExposureProgram::NotSet;
}
void MicroExif::setExposureProgram(const ExposureProgram &ep)
{
if (ep == ExposureProgram::NotSet)
m_exifTags.remove(EXIF_EXPOSUREPROGRAM);
else
m_exifTags.insert(EXIF_EXPOSUREPROGRAM, quint16(ep));
}
double MicroExif::exposureTime() const
{
if (!m_exifTags.contains(EXIF_EXPOSURETIME))
return qQNaN();
return m_exifTags.value(EXIF_EXPOSURETIME).toDouble();
}
void MicroExif::setExposureTime(double et)
{
if (qIsNaN(et))
m_exifTags.remove(EXIF_EXPOSURETIME);
else
m_exifTags.insert(EXIF_EXPOSURETIME, et);
}
double MicroExif::fNumber() const
{
if (!m_exifTags.contains(EXIF_FNUMBER))
return qQNaN();
return m_exifTags.value(EXIF_FNUMBER).toDouble();
}
void MicroExif::setFNumber(double f)
{
if (qIsNaN(f))
m_exifTags.remove(EXIF_FNUMBER);
else
m_exifTags.insert(EXIF_FNUMBER, f);
}
double MicroExif::focalLength() const
{
if (!m_exifTags.contains(EXIF_FOCALLENGTH))
return qQNaN();
return m_exifTags.value(EXIF_FOCALLENGTH).toDouble();
}
void MicroExif::setFocalLength(double fl)
{
if (qIsNaN(fl))
m_exifTags.remove(EXIF_FOCALLENGTH);
else
m_exifTags.insert(EXIF_FOCALLENGTH, fl);
}
FlashFlags MicroExif::flash() const
{
return FlashFlags(m_exifTags.value(EXIF_FLASH).toUInt());
}
void MicroExif::setFlash(const FlashFlags &flash)
{
if (flash == Flash::NotSet)
m_exifTags.remove(EXIF_FLASH);
else
m_exifTags.insert(EXIF_FLASH, quint16(flash));
}
WhiteBalance MicroExif::whiteBalance() const
{
auto ok = false;
auto v = m_exifTags.value(EXIF_WHITEBALANCE).toUInt(&ok);
return ok ? WhiteBalance(v) : WhiteBalance::NotSet;
}
void MicroExif::setWhiteBalance(const WhiteBalance &wb)
{
if (wb == WhiteBalance::NotSet)
m_exifTags.remove(EXIF_WHITEBALANCE);
else
m_exifTags.insert(EXIF_WHITEBALANCE, quint16(wb));
}
double MicroExif::latitude() const
{
auto ref = gpsString(GPS_LATITUDEREF).toUpper();
@@ -1045,6 +1199,30 @@ void MicroExif::setAltitude(double meters)
m_gpsTags.insert(GPS_ALTITUDE, meters);
}
double MicroExif::imageSpeed() const
{
if (!m_gpsTags.contains(GPS_SPEED))
return qQNaN();
auto ref = gpsString(GPS_SPEEDREF).toUpper();
auto speed = m_gpsTags.value(GPS_SPEED).toDouble();
if (ref == QStringLiteral("M"))
speed *= 1.60934;
else if (ref == QStringLiteral("N"))
speed *= 1.852;
return speed;
}
void MicroExif::setImageSpeed(double kmh)
{
if (qIsNaN(kmh)) {
m_gpsTags.remove(GPS_SPEEDREF);
m_gpsTags.remove(GPS_SPEED);
return;
}
m_gpsTags.insert(GPS_SPEEDREF, QStringLiteral("K"));
m_gpsTags.insert(GPS_SPEED, kmh);
}
double MicroExif::imageDirection(bool *isMagnetic) const
{
auto tmp = false;
@@ -1191,6 +1369,58 @@ void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) c
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_DIRECTION), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_SPEED)).isEmpty()) {
auto v = imageSpeed();
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_SPEED), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
// shot info
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_DIGITALZOOMRATIO)).isEmpty()) {
auto v = digitalZoomRatio();
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_DIGITALZOOMRATIO), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_EXPOSUREMODE)).isEmpty()) {
auto v = exposureMode();
if (v != ExposureMode::NotSet)
targetImage.setText(QStringLiteral(META_KEY_EXPOSUREMODE), QStringLiteral("%1").arg(quint16(v)));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_EXPOSUREPROGRAM)).isEmpty()) {
auto v = exposureProgram();
if (v != ExposureProgram::NotSet)
targetImage.setText(QStringLiteral(META_KEY_EXPOSUREPROGRAM), QStringLiteral("%1").arg(quint16(v)));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_EXPOSURETIME)).isEmpty()) {
auto v = exposureTime();
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_EXPOSURETIME), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_FLASH)).isEmpty()) {
auto v = flash();
if (v != Flash::NotSet)
targetImage.setText(QStringLiteral(META_KEY_FLASH), QStringLiteral("%1").arg(quint16(v)));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_FNUMBER)).isEmpty()) {
auto v = fNumber();
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_FNUMBER), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_FOCALLENGTH)).isEmpty()) {
auto v = focalLength();
if (!qIsNaN(v))
targetImage.setText(QStringLiteral(META_KEY_FOCALLENGTH), QStringLiteral("%1").arg(v, 0, 'g', 9));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_ISOSPEEDRATINGS)).isEmpty()) {
auto v = isoSpeedRatings();
if (v != 0)
targetImage.setText(QStringLiteral(META_KEY_ISOSPEEDRATINGS), QStringLiteral("%1").arg(v));
}
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_WHITEBALANCE)).isEmpty()) {
auto v = whiteBalance();
if (v != WhiteBalance::NotSet)
targetImage.setText(QStringLiteral(META_KEY_WHITEBALANCE), QStringLiteral("%1").arg(quint16(v)));
}
}
bool MicroExif::updateImageResolution(QImage &targetImage)
@@ -1217,7 +1447,7 @@ MicroExif MicroExif::fromByteArray(const QByteArray &ba, bool searchHeader)
idx = std::min(idxLE, idxBE);
else
idx = idxLE > -1 ? idxLE : idxBE;
if(idx > 0)
if (idx > 0)
ba0 = ba0.mid(idx);
}
QBuffer buf;
@@ -1308,7 +1538,7 @@ MicroExif MicroExif::fromImage(const QImage &image)
dt = QDateTime::currentDateTime();
exif.setDateTimeOriginal(dt);
// GPS Info
// GPS info
auto ok = false;
auto alt = image.text(QStringLiteral(META_KEY_ALTITUDE)).toDouble(&ok);
if (ok)
@@ -1322,6 +1552,38 @@ MicroExif MicroExif::fromImage(const QImage &image)
auto dir = image.text(QStringLiteral(META_KEY_DIRECTION)).toDouble(&ok);
if (ok)
exif.setImageDirection(dir);
auto spd = image.text(QStringLiteral(META_KEY_SPEED)).toDouble(&ok);
if (ok)
exif.setImageSpeed(spd);
// EXIF shot info
auto zoom = image.text(QStringLiteral(META_KEY_DIGITALZOOMRATIO)).toDouble(&ok);
if (ok)
exif.setDigitalZoomRatio(zoom);
auto expm = image.text(QStringLiteral(META_KEY_EXPOSUREMODE)).toUShort(&ok);
if (ok)
exif.setExposureMode(ExposureMode(expm));
auto expp = image.text(QStringLiteral(META_KEY_EXPOSUREPROGRAM)).toUShort(&ok);
if (ok)
exif.setExposureProgram(ExposureProgram(expp));
auto expt = image.text(QStringLiteral(META_KEY_EXPOSURETIME)).toDouble(&ok);
if (ok)
exif.setExposureTime(expt);
auto flsh = image.text(QStringLiteral(META_KEY_FLASH)).toUShort(&ok);
if (ok)
exif.setFlash(FlashFlags(flsh));
auto fnum = image.text(QStringLiteral(META_KEY_FNUMBER)).toDouble(&ok);
if (ok)
exif.setFNumber(fnum);
auto flen = image.text(QStringLiteral(META_KEY_FOCALLENGTH)).toDouble(&ok);
if (ok)
exif.setFocalLength(flen);
auto isos = image.text(QStringLiteral(META_KEY_ISOSPEEDRATINGS)).toUShort(&ok);
if (ok)
exif.setIsoSpeedRatings(isos);
auto whtb = image.text(QStringLiteral(META_KEY_WHITEBALANCE)).toUShort(&ok);
if (ok)
exif.setWhiteBalance(WhiteBalance(whtb));
return exif;
}

View File

@@ -24,6 +24,85 @@
#define EXIF_DEFAULT_BYTEORDER QDataStream::BigEndian
#endif
/*!
* \brief The Flash enum
*/
enum class Flash : quint16 {
NotSet = 0,
// Values for bit 0 indicating whether the flash fired.
// 0b = Flash did not fire.
// 1b = Flash fired.
Fired = 1,
// Values for bits 1 and 2 indicating the status of returned light.
// 00b = No strobe return detection function
// 01b = reserved
// 10b = Strobe return light not detected.
// 11b = Strobe return light detected.
ReturnLightNotDetected = 2 << 1,
ReturnLightDetected = 3 << 1,
// Values for bits 3 and 4 indicating the camera's flash mode.
// 00b = unknown
// 01b = Compulsory flash firing
// 10b = Compulsory flash suppression
// 11b = Auto mode
CompulsoryFiring = 1 << 3,
CompulsorySuppression = 2 << 3,
AutoMode = 3 << 3,
// Values for bit 5 indicating the presence of a flash function.
// 0b = Flash function present
// 1b = No flash function
FlashNotAvailable = 1 << 5,
// Values for bit 6 indicating the camera's red-eye mode.
// 0b = No red-eye reduction mode or unknown
// 1b = Red-eye reduction supported
RedEyeReductionSupported = 1 << 6,
};
Q_DECLARE_FLAGS(FlashFlags, Flash)
Q_DECLARE_OPERATORS_FOR_FLAGS(FlashFlags)
/*!
* \brief The ExposureMode enum
*/
enum class ExposureMode : quint16 {
Auto,
Manual,
AutoBracket,
NotSet = 65535
};
/*!
* \brief The ExposureProgram enum
*/
enum class ExposureProgram : quint16 {
NotDefined,
Manual,
Normal,
AperturePriority,
ShutterPriority,
Creative,
Action,
PortraitMode,
LandscapeMode,
NotSet = 65535
};
/*!
* \brief The WhiteBalance enum
*/
enum class WhiteBalance : quint16 {
Auto,
Manual,
NotSet = 65535
};
/*!
* \brief The MicroExif class
* Class to extract / write minimal EXIF data (e.g. resolution, rotation,
@@ -236,6 +315,69 @@ public:
QUuid uniqueId() const;
void setUniqueId(const QUuid &uuid);
/*!
* \brief digitalZoomRatio
* \return The digital zoom ratio when the image was shot or NaN if not set.
*/
double digitalZoomRatio() const;
void setDigitalZoomRatio(double zoom);
/*!
* \brief exposureMode
* \return The exposure mode set when the image was shot. In auto-bracketing mode, the camera shoots a series of frames of the same scene at different exposure settings.
*/
ExposureMode exposureMode() const;
void setExposureMode(const ExposureMode& em);
/*!
* \brief exposureProgram
* \return The class of the program used by the camera to set exposure when the picture is taken.
*/
ExposureProgram exposureProgram() const;
void setExposureProgram(const ExposureProgram& ep);
/*!
* \brief exposureTime
* \return Exposure time, given in seconds (sec) or NaN if not set.
*/
double exposureTime() const;
void setExposureTime(double et);
/*!
* \brief fNumber
* \return The F number or NaN if not set.
*/
double fNumber() const;
void setFNumber(double f);
/*!
* \brief focalLength
* \return The actual focal length of the lens, in mm.
*/
double focalLength() const;
void setFocalLength(double fl);
/*!
* \brief flash
* \return The status of flash when the image was shot.
*/
FlashFlags flash() const;
void setFlash(const FlashFlags& flash);
/*!
* \brief isoSpeedRatings
* \return The sensitivity of the camera or input device when the image was shot.
*/
quint16 isoSpeedRatings() const;
void setIsoSpeedRatings(quint16 iso);
/*!
* \brief whiteBalance
* \return The white balance mode set when the image was shot.
*/
WhiteBalance whiteBalance() const;
void setWhiteBalance(const WhiteBalance& wb);
/*!
* \brief latitude
* \return Floating-point number indicating the latitude in degrees north of the equator (e.g. 27.717) or NaN if not set.
@@ -258,6 +400,13 @@ public:
double altitude() const;
void setAltitude(double meters);
/*!
* \brief imageSpeed
* \return The speed in Km/h or NaN if not set.
*/
double imageSpeed() const;
void setImageSpeed(double kmh);
/*!
* \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.

View File

@@ -1017,7 +1017,7 @@ inline void rawChannelCopy(uchar *target, qint32 targetChannels, qint32 targetCh
template<class T>
inline void cmykToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
inline bool cmykToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
{
auto s = reinterpret_cast<const T*>(source);
auto t = reinterpret_cast<T*>(target);
@@ -1026,7 +1026,7 @@ inline void cmykToRgb(uchar *target, qint32 targetChannels, const char *source,
if (sourceChannels < 2) {
qCDebug(LOG_PSDPLUGIN) << "cmykToRgb: image is not a valid MCH/CMYK!";
return;
return false;
}
for (qint32 w = 0; w < width; ++w) {
@@ -1047,6 +1047,7 @@ inline void cmykToRgb(uchar *target, qint32 targetChannels, const char *source,
*(pt + 3) = std::numeric_limits<T>::max();
}
}
return true;
}
inline double finv(double v)
@@ -1066,7 +1067,7 @@ inline double gammaCorrection(double linear)
}
template<class T>
inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
inline bool labToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
{
auto s = reinterpret_cast<const T*>(source);
auto t = reinterpret_cast<T*>(target);
@@ -1075,7 +1076,7 @@ inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, q
if (sourceChannels < 3) {
qCDebug(LOG_PSDPLUGIN) << "labToRgb: image is not a valid LAB!";
return;
return false;
}
for (qint32 w = 0; w < width; ++w) {
@@ -1110,6 +1111,7 @@ inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, q
*(pt + 3) = std::numeric_limits<T>::max();
}
}
return true;
}
bool readChannel(QByteArray &target, QDataStream &stream, quint32 compressedSize, quint16 compression)
@@ -1450,10 +1452,13 @@ bool PSDHandler::read(QImage *image)
// Conversion to RGB
if (header.color_mode == CM_CMYK || header.color_mode == CM_MULTICHANNEL) {
if (tmpCmyk.isNull()) {
if (header.depth == 8)
cmykToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
else if (header.depth == 16)
cmykToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
if (header.depth == 8) {
if (!cmykToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
return false;
} else if (header.depth == 16) {
if (!cmykToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
return false;
}
} else if (header.depth == 8) {
rawChannelsCopyToCMYK<quint8>(tmpCmyk.bits(), 4, psdScanline.data(), header.channel_count, header.width);
if (auto rgbPtr = iccConv.convertedScanLine(tmpCmyk, 0))
@@ -1469,10 +1474,13 @@ bool PSDHandler::read(QImage *image)
}
}
if (header.color_mode == CM_LABCOLOR) {
if (header.depth == 8)
labToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
else if (header.depth == 16)
labToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
if (header.depth == 8) {
if (!labToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
return false;
} else if (header.depth == 16) {
if (!labToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
return false;
}
}
if (header.color_mode == CM_RGB) {
if (header.depth == 8)

View File

@@ -319,7 +319,10 @@ bool SGIImagePrivate::readImage(QImage &img)
if (_rle) {
uint l;
_starttab = new quint32[_numrows];
_starttab = new (std::nothrow) quint32[_numrows];
if (_starttab == nullptr) {
return false;
}
for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
_stream >> _starttab[l];
_starttab[l] -= 512 + _numrows * 2 * sizeof(quint32);
@@ -331,7 +334,10 @@ bool SGIImagePrivate::readImage(QImage &img)
_starttab[l] = 0;
}
_lengthtab = new quint32[_numrows];
_lengthtab = new (std::nothrow) quint32[_numrows];
if (_lengthtab == nullptr) {
return false;
}
for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
_stream >> _lengthtab[l];
if (_stream.status() != QDataStream::Ok) {
@@ -794,7 +800,10 @@ bool SGIImagePrivate::writeImage(const QImage &image)
_pixmax = 0;
_colormap = NORMAL;
_numrows = _ysize * _zsize;
_starttab = new quint32[_numrows];
_starttab = new (std::nothrow) quint32[_numrows];
if (_starttab == nullptr) {
return false;
}
_rlemap.setBaseOffset(512 + _numrows * 2 * sizeof(quint32));
if (!scanData(image, tfmt, tcs)) {

View File

@@ -731,7 +731,7 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img)
if (div == 0)
hasAlpha = false;
for (int x = x_start; x != x_end; x += x_step) {
const int alpha = hasAlpha ? int((src[3]) << (8 - numAlphaBits)) * 255 / div : 255;
const int alpha = hasAlpha ? int(quint8(src[3]) << (8 - numAlphaBits)) * 255 / div : 255;
scanline[x] = qRgba(src[2], src[1], src[0], alpha);
src += 4;
}

398
src/imageformats/tim.cpp Normal file
View File

@@ -0,0 +1,398 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "tim_p.h"
#include "util_p.h"
#include <QIODevice>
#include <QImage>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(LOG_TIMPLUGIN)
Q_LOGGING_CATEGORY(LOG_TIMPLUGIN, "kf.imageformats.plugins.tim", QtWarningMsg)
#define TYPE_4BPP 0 // never seen
#define TYPE_IDX_4BPP 8
#define TYPE_8BPP 1 // never seen
#define TYPE_IDX_8BPP 9
#define TYPE_16BPP 2
#define TYPE_24BPP 3
#define HEADER_SIZE 20
class TIMHeader
{
private:
QByteArray m_rawHeader;
quint16 ui16(quint8 c1, quint8 c2) const {
return (quint16(c2) << 8) | quint16(c1);
}
quint32 ui32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const {
return (quint32(c4) << 24) | (quint32(c3) << 16) | (quint32(c2) << 8) | quint32(c1);
}
public:
TIMHeader()
{
}
quint32 type() const
{
if (m_rawHeader.size() < HEADER_SIZE) {
return 0;
}
return ui32(m_rawHeader.at(4), m_rawHeader.at(5), m_rawHeader.at(6), m_rawHeader.at(7)) & 0xF;
}
quint32 offset() const
{
if (m_rawHeader.size() < HEADER_SIZE) {
return 0;
}
auto o = quint32(HEADER_SIZE);
auto t = type();
if (t == TYPE_IDX_4BPP || t == TYPE_IDX_8BPP) { // indexed
o += ui32(m_rawHeader.at(8), m_rawHeader.at(9), m_rawHeader.at(10), m_rawHeader.at(11));
}
return o;
}
bool isValid(quint32 size = 0) const
{
if (m_rawHeader.size() < HEADER_SIZE) {
return false;
}
if (size == 0) {
size = offset();
}
if (m_rawHeader.size() < size) {
return false;
}
return (m_rawHeader.startsWith(QByteArray::fromRawData("\x10\x00\x00\x00", 4)));
}
bool isSupported() const
{
return format() != QImage::Format_Invalid;
}
qint32 width() const
{
auto strideLen = strideSize();
auto t = type();
if (t == TYPE_4BPP || t == TYPE_IDX_4BPP) {
return strideLen * 2;
}
if (t == TYPE_8BPP || t == TYPE_IDX_8BPP) {
return strideLen;
}
if (t == TYPE_24BPP) {
return strideLen / 3;
}
return strideLen / 2;
}
qint32 height() const
{
auto o = offset();
if (!isValid(o)) {
return 0;
}
return qint32(ui16(m_rawHeader.at(o - 2), m_rawHeader.at(o - 1)));
}
QSize size() const
{
return QSize(width(), height());
}
QImage::Format format() const
{
auto t = type();
if (t == TYPE_IDX_4BPP || t == TYPE_IDX_8BPP || t == TYPE_4BPP) {
return QImage::Format_Indexed8;
}
if (t == TYPE_IDX_8BPP) {
return QImage::Format_Grayscale8;
}
if (t == TYPE_16BPP) {
return QImage::Format_RGB555;
}
if (t == TYPE_24BPP) {
return QImage::Format_RGB888;
}
return QImage::Format_Invalid;
}
quint32 strideSize() const
{
auto o = offset();
if (!isValid(o)) {
return 0;
}
return ui16(m_rawHeader.at(o - 4), m_rawHeader.at(o - 3)) * 2;
}
qint32 paletteColors() const
{
if (this->format() != QImage::Format_Indexed8) {
return 0;
}
return qint32(ui16(m_rawHeader.at(16), m_rawHeader.at(17)));
}
qint32 paletteCount() const
{
if (this->format() != QImage::Format_Indexed8) {
return 0;
}
return qint32(ui16(m_rawHeader.at(18), m_rawHeader.at(19)));
}
QList<QRgb> palette() const
{
if (format() != QImage::Format_Indexed8) {
return {};
}
// 4bpp without CLUT is treated as indexed
if (type() == TYPE_4BPP) {
QList<QRgb> pal;
for (auto i = 0; i < 16; ++i) {
auto v = i * 17;
pal << qRgb(v, v, v);
}
return pal;
}
// read the first paette only
auto len = paletteColors();
if (!isValid(HEADER_SIZE + len * 2)) {
return {};
}
QList<QRgb> clut;
for (auto i = 0; i < len; ++i) {
auto v = ui16(m_rawHeader.at(HEADER_SIZE + i * 2), m_rawHeader.at(HEADER_SIZE + i * 2 + 1));
// in some specs, the bit 15 is the alpha but with the image sample used, transparencies appear
// where there shouldn't be any (so, disabled for now)
clut << qRgba((v & 0x1F) * 255 / 31, ((v >> 5) & 0x1F) * 255 / 31, ((v >> 10) & 0x1F) * 255 / 31, 255);
}
return clut;
}
bool read(QIODevice *d)
{
m_rawHeader = d->read(HEADER_SIZE);
if (m_rawHeader.size() != HEADER_SIZE) {
return false;
}
auto o = offset() - HEADER_SIZE;
if (o > kMaxQVectorSize - HEADER_SIZE) {
return false;
}
m_rawHeader.append(d->read(o));
return isValid();
}
bool peek(QIODevice *d)
{
m_rawHeader = d->peek(HEADER_SIZE);
if (m_rawHeader.size() != HEADER_SIZE) {
return false;
}
auto o = offset();
if (o > kMaxQVectorSize - HEADER_SIZE) {
return false;
}
if (o > m_rawHeader.size()) {
m_rawHeader = d->peek(o);
}
return isValid();
}
bool jumpToImageData(QIODevice *d) const
{
if (d->isSequential()) {
if (auto sz = std::max(offset() - quint32(m_rawHeader.size()), quint32())) {
return d->read(sz).size() == sz;
}
return true;
}
return d->seek(offset());
}
};
class TIMHandlerPrivate
{
public:
TIMHandlerPrivate() {}
~TIMHandlerPrivate() {}
TIMHeader m_header;
};
TIMHandler::TIMHandler()
: QImageIOHandler()
, d(new TIMHandlerPrivate)
{
}
bool TIMHandler::canRead() const
{
if (canRead(device())) {
setFormat("tim");
return true;
}
return false;
}
bool TIMHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::canRead() called with no device";
return false;
}
TIMHeader h;
if (!h.peek(device)) {
return false;
}
return h.isSupported();
}
bool TIMHandler::read(QImage *image)
{
auto&& header = d->m_header;
if (!header.read(device())) {
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() invalid header";
return false;
}
auto img = imageAlloc(header.size(), header.format());
if (img.isNull()) {
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while allocating the image";
return false;
}
if (img.format() == QImage::Format_Indexed8) {
auto pal = header.palette();
if (pal.isEmpty()) {
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while reading the palette";
return false;
}
img.setColorTable(pal);
}
auto d = device();
if (!header.jumpToImageData(d)) {
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while seeking image data";
return false;
}
auto size = std::min(img.bytesPerLine(), qsizetype(header.strideSize()));
QByteArray tmpBuff;
auto conv_4bpp = (header.type() == TYPE_4BPP || header.type() == TYPE_IDX_4BPP);
if (conv_4bpp && size * 2 <= img.bytesPerLine()) {
tmpBuff.resize(size);
}
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
auto tbuf = tmpBuff.isEmpty() ? line : tmpBuff.data();
if (d->read(tbuf, size) != size) {
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while reading image scanline";
return false;
}
if (conv_4bpp) {
for (auto x = 0, w = qint32(tmpBuff.size()); x < w; ++x) {
auto &&v = tmpBuff.at(x);
line[x * 2 + 1] = (v >> 4) & 0xF;
line[x * 2] = v & 0xF;
}
}
}
if (img.format() == QImage::Format_RGB555) {
img.rgbSwap();
}
*image = img;
return true;
}
bool TIMHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
return false;
}
QVariant TIMHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
auto&& h = d->m_header;
if (h.isValid()) {
v = QVariant::fromValue(h.size());
} else if (auto d = device()) {
if (h.peek(d)) {
v = QVariant::fromValue(h.size());
}
}
}
if (option == QImageIOHandler::ImageFormat) {
auto&& h = d->m_header;
if (h.isValid()) {
v = QVariant::fromValue(h.format());
} else if (auto d = device()) {
if (h.peek(d)) {
v = QVariant::fromValue(h.format());
}
}
}
return v;
}
QImageIOPlugin::Capabilities TIMPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "tim") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && TIMHandler::canRead(device)) {
cap |= CanRead;
}
return cap;
}
QImageIOHandler *TIMPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new TIMHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_tim_p.cpp"

View File

@@ -0,0 +1,4 @@
{
"Keys": [ "tim" ],
"MimeTypes": [ "image/x-tim" ]
}

42
src/imageformats/tim_p.h Normal file
View File

@@ -0,0 +1,42 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KIMG_TIM_P_H
#define KIMG_TIM_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class TIMHandlerPrivate;
class TIMHandler : public QImageIOHandler
{
public:
TIMHandler();
bool canRead() const override;
bool read(QImage *image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<TIMHandlerPrivate> d;
};
class TIMPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "tim.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_TIM_P_H

View File

@@ -34,10 +34,22 @@
#define META_KEY_MODIFICATIONDATE "ModificationDate"
#define META_KEY_OWNER "Owner"
#define META_KEY_SOFTWARE "Software"
#define META_KEY_SPEED "Speed"
#define META_KEY_TITLE "Title"
#define META_KEY_XML_GIMP "XML:org.gimp.xml"
#define META_KEY_XMP_ADOBE "XML:com.adobe.xmp"
// Shot info metadata keys
#define META_KEY_DIGITALZOOMRATIO "DigitalZoomRatio"
#define META_KEY_EXPOSUREMODE "ExposureMode"
#define META_KEY_EXPOSUREPROGRAM "ExposureProgram"
#define META_KEY_EXPOSURETIME "ExposureTime"
#define META_KEY_FLASH "Flash"
#define META_KEY_FNUMBER "FNumber"
#define META_KEY_FOCALLENGTH "FocalLength"
#define META_KEY_ISOSPEEDRATINGS "ISOSpeedRatings"
#define META_KEY_WHITEBALANCE "WhiteBalance"
// Camera info metadata keys
#define META_KEY_MANUFACTURER "Manufacturer"
#define META_KEY_MODEL "Model"