Compare commits

..

27 Commits

Author SHA1 Message Date
Nicolas Fella
31038071fa Enable LSAN in CI
To check for leaks
2026-05-27 16:46:37 +02:00
Mirco Miranda
9dfcf67ea9 EXR: reject files with dimensions exceeding 300kx300k pixels 2026-05-25 14:13:15 +02:00
Mirco Miranda
6017099044 OSS Fuzz: set image allocation limit to 2000 MiB 2026-05-23 05:59:01 +02:00
Mirco Miranda
9ddad16767 Improve size limits for AVIF, HEIF, and RAW plugins 2026-05-15 12:13:12 +02:00
Mirco Miranda
0e2b137b32 IFF: fix byte swapping in 16-bit DEEP images 2026-05-12 10:31:43 +02:00
Mirco Miranda
6d5e61f0b0 Add Farbfeld read only support 2026-05-11 07:38:28 +02:00
Mirco Miranda
d7c3174fb6 DDS: fix mime type 2026-05-08 04:58:39 +02:00
Mirco Miranda
49a8fd38c8 IFF: DEEP image support 2026-05-08 01:29:54 +02:00
Mirco Miranda
1206598337 imageAlloc: add image initialization support 2026-05-07 08:41:10 +02:00
Mirco Miranda
3488077d8d Fix uninitialized value 2026-05-04 08:51:34 +02:00
Laurent Montel
ea8a4dccdc GIT_SILENT: Bump kf ecm_set_disabled_deprecation_versions. Make sure that it compiles fine without kf 6.26 deprecated methods 2026-05-03 08:41:08 +02:00
Mirco Miranda
8048279473 HEIF: image transformation support 2026-05-03 00:00:11 +02:00
Nicolas Fella
18d0f93d60 Update CI image 2026-05-02 15:30:10 +02:00
Nicolas Fella
497e612ad3 Update version to 6.27.0 2026-05-01 22:24:21 +02:00
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
79 changed files with 2390 additions and 190 deletions

View File

@@ -16,7 +16,7 @@ include:
image_json_validate:
stage: validate
image: invent-registry.kde.org/sysadmin/ci-images/suse-qt69:latest
image: invent-registry.kde.org/sysadmin/ci-images/suse-qt611:latest
tags:
- Linux
script:

View File

@@ -7,5 +7,6 @@ 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
enable-lsan: True

View File

@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.25.0") # handled by release scripts
set(KF_DEP_VERSION "6.25.0") # handled by release scripts
set(KF_VERSION "6.27.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.25.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)
@@ -72,7 +72,7 @@ set_property(CACHE KIMAGEFORMATS_HEJ2_TEST PROPERTY STRINGS "OFF" "READ_ONLY" "A
set(KIMAGEFORMATS_AVCI_TEST "ALL" CACHE STRING "Enable AVCI tests: OFF, ALL")
set_property(CACHE KIMAGEFORMATS_AVCI_TEST PROPERTY STRINGS "OFF" "ALL")
if(KIMAGEFORMATS_HEIF)
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.10.0)
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.17.0)
endif()
add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/HEIC images")
@@ -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.23.0
KF 6.26.0
)
add_subdirectory(src)

View File

@@ -16,6 +16,7 @@ The following image formats have read-only support:
- Animated Windows cursors (ani)
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
- Farbfeld (ff)
- Gimp (xcf)
- Interchange Format Files (iff, ilbm, lbm)
- Krita (kra)
@@ -155,8 +156,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.
@@ -224,17 +248,19 @@ Anyway, all plugins are also limited by the
> [!note]
> You can change the maximum limit of 300000 pixels by setting the constant
> `KIF_LARGE_IMAGE_PIXEL_LIMIT` to the desired value in the cmake file.
> `KIF_LARGE_IMAGE_PIXEL_LIMIT` to the desired value in the cmake file. It
> cannot be less than 65536.
Below are the maximum sizes for each plugin ('n/a' means no limit, i.e. the
limit depends on the format encoding).
- ANI: n/a
- ANI: same size as Qt's ICO plugin
- AVIF: 32,768 x 32,768 pixels, in any case no larger than 256 megapixels
- DDS: 300,000 x 300,000 pixels
- EXR: 300,000 x 300,000 pixels
- EPS: same size as Qt's JPG plugin
- FF: 300,000 x 300,000 pixels
- HDR: 300,000 x 300,000 pixels
- HEIF: n/a
- HEIF: 65,535 x 65,535 pixels
- IFF: 65,535 x 65,535 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
@@ -295,6 +321,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
@@ -305,6 +336,15 @@ distributions. In particular, it is necessary that the HEIF library has
support for HEVC codec. If HEVC codec is not available the plugin
will compile but will fail the tests.
The following defines can be defined in cmake to modify the behavior of the
plugin:
- `HEIF_DISABLE_QT_TRANSFORMATION`: HEIF transformations, in addition to
rotations and reflections, also support image cropping. Consequently, the
Qt plugin, must also honor the crop. This define is useful in case
of problems: activating it disables Qt's support for transformations,
delegating them to the HEIF libraries (which will therefore always apply
them regardless of what is requested from Qt).
**If you are interested in compiling the plugin without running the tests,
also use the following string options:**
- `KIMAGEFORMATS_HEIF_TEST` to change the behaviour of HEIF tests. Set to
@@ -324,6 +364,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
@@ -362,6 +406,7 @@ The plugin supports the following image data:
- FORM IMAG (Compact Disc-Interactive): It supports CLut4, CLut7, CLut8, Rle7
and DYuv formats.
- FORM RGFX: It supports uncompressed images only.
- FORM DEEP: It supports uncompressed, RLE and TVDC images.
- FOR4 CIMG (Maya Image File Format): It supports 24/48-bit RGB and 32/64-bit
RGBA images.
@@ -379,6 +424,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
@@ -394,7 +444,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:
@@ -443,6 +498,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

@@ -76,6 +76,7 @@ endmacro()
# Loads each <format> image in read/<format>/, and compares the
# result against the data read from the corresponding png file
kimageformats_read_tests(
ff
hdr
iff
pcx

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

@@ -162,6 +162,7 @@ HANDLER_TYPES="ANIHandler ani
QAVIFHandler avif
QDDSHandler dds
EXRHandler exr
FFHandler ff
HDRHandler hdr
HEIFHandler heif
IFFHandler iff

View File

@@ -23,17 +23,19 @@
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|tim|tga|xcf]_fuzzer
python infra/helper.py run_fuzzer kimageformats kimgio_[ani|avif|dds|exr|ff|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tim|tga|xcf]_fuzzer
*/
#include <QBuffer>
#include <QCoreApplication>
#include <QImage>
#include <QImageReader>
#include "ani_p.h"
#include "avif_p.h"
#include "dds_p.h"
#include "exr_p.h"
#include "ff_p.h"
#include "hdr_p.h"
#include "heif_p.h"
#include "iff_p.h"
@@ -61,6 +63,8 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
int argc = 0;
QCoreApplication a(argc, nullptr);
QImageReader::setAllocationLimit(2000);
QImageIOHandler* handler = new HANDLER();
QImage i;

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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

View File

@@ -0,0 +1,17 @@
[
{
"fileName" : "orientation_all.png",
"colorSpace" : {
"description" : "GIMP built-in sRGB",
"primaries" : "SRgb",
"transferFunction" : "SRgb",
"gamma" : 0
},
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

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"

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

@@ -73,6 +73,10 @@ endif()
##################################
kimageformats_add_plugin(kimg_ff SOURCES ff.cpp)
##################################
kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
##################################

View File

@@ -49,6 +49,16 @@ Quality range - compression/subsampling
#define KIMG_AVIF_QUALITY_LOW 51
#endif
/* *** AVIF_MAX_IMAGE_WIDTH and AVIF_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
#ifndef AVIF_MAX_IMAGE_WIDTH
#define AVIF_MAX_IMAGE_WIDTH KIF_64K_IMAGE_PIXEL_LIMIT
#endif
#ifndef AVIF_MAX_IMAGE_HEIGHT
#define AVIF_MAX_IMAGE_HEIGHT AVIF_MAX_IMAGE_WIDTH
#endif
QAVIFHandler::QAVIFHandler()
: m_parseState(ParseAvifNotParsed)
, m_quality(KIMG_AVIF_DEFAULT_QUALITY)
@@ -168,7 +178,7 @@ bool QAVIFHandler::ensureDecoder()
#endif
#if AVIF_VERSION >= 110000
m_decoder->imageDimensionLimit = 65535;
m_decoder->imageDimensionLimit = std::max(AVIF_MAX_IMAGE_WIDTH, AVIF_MAX_IMAGE_HEIGHT) - 1;
#endif
avifResult decodeResult;
@@ -196,7 +206,7 @@ bool QAVIFHandler::ensureDecoder()
m_container_width = m_decoder->image->width;
m_container_height = m_decoder->image->height;
if ((m_container_width > 65535) || (m_container_height > 65535)) {
if ((m_container_width >= AVIF_MAX_IMAGE_WIDTH) || (m_container_height >= AVIF_MAX_IMAGE_HEIGHT)) {
qCWarning(LOG_AVIFPLUGIN, "AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
m_parseState = ParseAvifError;
return false;
@@ -605,7 +615,7 @@ bool QAVIFHandler::write(const QImage &image)
}
if ((image.width() > 0) && (image.height() > 0)) {
if ((image.width() > 65535) || (image.height() > 65535)) {
if ((image.width() >= AVIF_MAX_IMAGE_WIDTH) || (image.height() >= AVIF_MAX_IMAGE_HEIGHT)) {
qCWarning(LOG_AVIFPLUGIN, "Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}

View File

@@ -295,6 +295,14 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new CTBLChunk());
} else if (cid == DATE_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DATEChunk());
} else if (cid == DBOD_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DBODChunk());
} else if (cid == DGBL_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DGBLChunk());
} else if (cid == DLOC_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DLOCChunk());
} else if (cid == DPEL_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DPELChunk());
} else if (cid == DPI__CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DPIChunk());
} else if (cid == EXIF_CHUNK) {
@@ -341,6 +349,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new SHAMChunk());
} else if (cid == TBHD_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new TBHDChunk());
} else if (cid == TVDC_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new TVDCChunk());
} else if (cid == VDAT_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new VDATChunk());
} else if (cid == VERS_CHUNK) {
@@ -1468,6 +1478,8 @@ bool FORMChunk::innerReadStructure(QIODevice *d)
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == RGFX_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == DEEP_FORM_TYPE || _type == TVPP_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
}
return ok;
}
@@ -1585,6 +1597,72 @@ QImage::Format FORMChunk::rgfxFormat() const
return QImage::Format_Invalid;
}
QImage::Format FORMChunk::deepFormat() const
{
auto pels = IFFChunk::searchT<DPELChunk>(chunks());
if (pels.isEmpty()) {
return QImage::Format_Invalid;
}
auto list = pels.first()->elements();
// support for same depth on all elements
auto depth = -1;
for (auto &&el : list) {
if (depth < 0)
depth = el.depth;
if (depth != el.depth)
return QImage::Format_Invalid;
}
// calculate the image format
if (list.size() == 4) {
if (list.at(0).type == DPELChunk::Red &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Blue &&
list.at(3).type == DPELChunk::Alpha) {
if (depth == 8)
return FORMAT_RGBA_8BIT;
else if (depth == 16)
return QImage::Format_RGBA64;
} else if (list.at(0).type == DPELChunk::Cyan &&
list.at(1).type == DPELChunk::Magenta &&
list.at(2).type == DPELChunk::Yellow &&
list.at(3).type == DPELChunk::Black) {
if (depth == 8)
return QImage::Format_CMYK8888;
} else if (list.at(0).type == DPELChunk::Red &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Blue) {
// unknown type of channel 4 -> ignoring it
if (depth == 8)
return QImage::Format_RGBX8888;
else if (depth == 16)
return QImage::Format_RGBX64;
}
} else if (list.size() == 3) {
if (list.at(0).type == DPELChunk::Red &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Blue) {
if (depth == 8)
return FORMAT_RGB_8BIT;
} else if (list.at(0).type == DPELChunk::Blue &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Red) {
if (depth == 8)
return QImage::Format_BGR888;
}
} else if (list.size() == 1) {
if (depth == 1)
return QImage::Format_Mono;
else if (depth == 8)
return QImage::Format_Grayscale8;
else if (depth == 16)
return QImage::Format_Grayscale16;
}
return QImage::Format_Invalid;
}
QByteArray FORMChunk::formType() const
{
return _type;
@@ -1596,6 +1674,8 @@ QImage::Format FORMChunk::format() const
return cdiFormat();
} else if (formType() == RGFX_FORM_TYPE) {
return rgfxFormat();
} else if (formType() == DEEP_FORM_TYPE || formType() == TVPP_FORM_TYPE) {
return deepFormat();
}
return iffFormat();
}
@@ -1612,6 +1692,15 @@ QSize FORMChunk::size() const
if (!rghds.isEmpty()) {
return rghds.first()->size();
}
} else if (formType() == DEEP_FORM_TYPE || formType() == TVPP_FORM_TYPE) {
auto dlocs = IFFChunk::searchT<DLOCChunk>(chunks());
if (!dlocs.isEmpty()) {
return dlocs.first()->size();
}
auto dgbls = IFFChunk::searchT<DGBLChunk>(chunks());
if (!dgbls.isEmpty()) {
return dgbls.first()->size();
}
} else {
auto bmhds = IFFChunk::searchT<BMHDChunk>(chunks());
if (!bmhds.isEmpty()) {
@@ -1735,6 +1824,8 @@ bool CATChunk::innerReadStructure(QIODevice *d)
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == RGFX_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == DEEP_FORM_TYPE || _type == TVPP_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
}
return ok;
}
@@ -3574,6 +3665,438 @@ quint32 RBODChunk::strideSize(const RGHDChunk *header) const
}
/* ******************
* *** DGBL Chunk ***
* ****************** */
DGBLChunk::~DGBLChunk()
{
}
DGBLChunk::DGBLChunk()
: IFFChunk()
{
}
DGBLChunk::Compression DGBLChunk::compression() const
{
if (!isValid()) {
return Compression::Uncompressed;
}
return Compression(ui16(data(), 4));
}
qint32 DGBLChunk::width() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 0));
}
qint32 DGBLChunk::height() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 2));
}
quint8 DGBLChunk::xAspectRatio() const
{
if (!isValid()) {
return 0;
}
return quint8(data().at(6));
}
quint8 DGBLChunk::yAspectRatio() const
{
if (!isValid()) {
return 0;
}
return quint8(data().at(7));
}
bool DGBLChunk::isValid() const
{
if (dataBytes() < 8) {
return false;
}
return chunkId() == DGBLChunk::defaultChunkId();
}
bool DGBLChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** DPEL Chunk ***
* ****************** */
DPELChunk::~DPELChunk()
{
}
DPELChunk::DPELChunk()
: IFFChunk()
{
}
qint32 DPELChunk::count() const
{
if (dataBytes() < 4) {
return 0;
}
auto cnt = i32(data(), 0);
if (cnt < 0 || cnt > 128) {
// an image should have 3, 4 or 5 channels:
// 128 is enough to give an error.
cnt = 0;
}
return cnt;
}
qint32 DPELChunk::depth() const
{
auto depth = 0;
auto list = elements();
for (auto &&el : list) {
depth += el.depth;
}
return depth;
}
QList<DPELChunk::Element> DPELChunk::elements() const
{
QList<DPELChunk::Element> list;
if (isValid()) {
for (auto n = count(), i = 0; i < n; ++i) {
auto idx = 4 + i * 4;
list << DPELChunk::Element(DataType(ui16(data(), idx)),
ui16(data(), idx + 2));
}
}
return list;
}
bool DPELChunk::isValid() const
{
if (dataBytes() < quint32(4 + count() * 4)) {
return false;
}
return chunkId() == DPELChunk::defaultChunkId();
}
bool DPELChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** DLOC Chunk ***
* ****************** */
DLOCChunk::~DLOCChunk()
{
}
DLOCChunk::DLOCChunk()
: IFFChunk()
{
}
qint32 DLOCChunk::width() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 0));
}
qint32 DLOCChunk::height() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 2));
}
QSize DLOCChunk::size() const
{
return QSize(width(), height());
}
qint32 DLOCChunk::xOffset() const
{
if (!isValid()) {
return 0;
}
return qint32(i16(data(), 4));
}
qint32 DLOCChunk::yOffset() const
{
if (!isValid()) {
return 0;
}
return qint32(i16(data(), 6));
}
bool DLOCChunk::isValid() const
{
if (dataBytes() < 8) {
return false;
}
return chunkId() == DLOCChunk::defaultChunkId();
}
bool DLOCChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** TVDC Chunk ***
* ****************** */
TVDCChunk::~TVDCChunk()
{
}
TVDCChunk::TVDCChunk()
: IFFChunk()
{
}
qint32 TVDCChunk::count() const
{
if (!isValid()) {
return 0;
}
return dataBytes() / 2;
}
QList<quint16> TVDCChunk::table() const
{
QList<quint16> list;
if (isValid()) {
for (auto n = count(), i = 0; i < n; ++i) {
list << ui16(data(), i * 2);
}
}
return list;
}
bool TVDCChunk::isValid() const
{
if (dataBytes() < 32) {
return false;
}
return chunkId() == TVDCChunk::defaultChunkId();
}
bool TVDCChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** DBOD Chunk ***
* ****************** */
DBODChunk::~DBODChunk()
{
}
DBODChunk::DBODChunk()
: IFFChunk()
{
}
bool DBODChunk::isValid() const
{
return chunkId() == DBODChunk::defaultChunkId();
}
/*!
* \brief rleDeepDecompress
* Each run contains a pixel (all components)
*/
inline qint64 rleDeepDecompress(QIODevice *input, char *output, qint64 olen, qint32 pixelBytes)
{
qint64 j = 0;
pixelBytes = std::max(1, pixelBytes);
for (qint64 rr = 0, available = olen; j < olen; available = olen - j) {
char n;
// check the output buffer space for the next run
if (available < 129 * pixelBytes) {
if (input->peek(&n, 1) != 1) { // end of data (or error)
break;
}
if ((static_cast<signed char>(n) >= 0 ? qint64(n) + 1 : qint64(1 - n)) * pixelBytes > available)
break;
}
// decompress
if (input->read(&n, 1) != 1) { // end of data (or error)
break;
}
if (static_cast<signed char>(n) >= 0) {
rr = input->read(output + j, (qint64(n) + 1) * pixelBytes);
if (rr == -1) {
return -1;
}
}
else {
auto buf = input->read(pixelBytes);
if (buf.size() != pixelBytes) {
break;
}
rr = qint64(1 - static_cast<signed char>(n));
for (qint64 i = 0; i < rr; ++i) {
std::memcpy(output + j + i * pixelBytes, buf.data(), buf.size());
}
rr *= pixelBytes;
}
j += rr;
}
return j;
}
/*!
* \brief tvdcDeepDecompress
* the compression is made line by line for each elementof the chunk DPEL.
* For RGBA for example we have a Red line, a Green line, and so on.
*/
inline qint64 tvdcDeepDecompress(QIODevice *input, char *output, qint64 olen, const QList<quint16>& table)
{
if (table.size() != 16) {
return -1;
}
quint8 v = 0;
quint8 last = 0;
for (qint64 i = 0, pos = 0, lastRead = -1; i < olen; ++i) {
if ((pos >> 1) != lastRead) {
char n;
if (input->read(&n, 1) != 1) {
return -1;
}
lastRead = (pos >> 1);
last = quint8(n);
}
quint8 d = last;
if (pos++ & 1) {
d &= 0xf;
} else {
d >>= 4;
}
v += table.at(d);
output[i] = char(v);
if (!table.at(d)) {
if ((pos >> 1) != lastRead) {
char n;
if (input->read(&n, 1) != 1) {
return -1;
}
lastRead = (pos >> 1);
last = quint8(n);
}
d = last;
if (pos++ & 1) {
d &= 0xf;
} else {
d >>= 4;
}
while (d--) {
if (i < olen - 1) {
output[++i] = char(v);
continue;
}
return -1;
}
}
}
return olen;
}
QByteArray DBODChunk::strideRead(QIODevice *d, qint32, const DGBLChunk *header, const DPELChunk *pel, const DLOCChunk *loc, const TVDCChunk *tvdc) const
{
auto size = strideSize(header, pel, loc);
if (size == 0) {
return {};
}
qint64 rr = 0;
QByteArray planes(size, char());
if (header->compression() == DGBLChunk::Compression::Uncompressed) {
rr = d->read(planes.data(), planes.size());
} else if (header->compression() == DGBLChunk::Compression::Rle) {
rr = rleDeepDecompress(d, planes.data(), planes.size(), pel->depth() / 8);
} else if (header->compression() == DGBLChunk::Compression::TvDeepCompression) {
if (tvdc) { // TVDC is planar, so I have to convert to chunky
auto table = tvdc->table();
for (auto i = 0, n = pel->count(); i < n; ++i) {
QByteArray ba(size / n, char());
rr += tvdcDeepDecompress(d, ba.data(), ba.size(), tvdc->table());
for (auto j = 0, m = int(ba.size()); j < m; ++j) {
planes[i + j * n] = ba.at(j);
}
}
}
} else {
qCDebug(LOG_IFFPLUGIN) << "DBODChunk::strideRead(): unknown compression" << header->compression();
}
// Uncompressed, Rle and TvDeepCompression: one line at a time.
if (rr != size) {
return {};
}
// byte swap
if (auto count = pel->count()) {
if (pel->depth() / count == 16) {
for (auto x = 0, w = qint32(planes.size()) - 1; x < w; x += 2) {
std::swap(planes[x], planes[x + 1]);
}
}
}
return planes;
}
bool DBODChunk::resetStrideRead(QIODevice *d) const
{
return seek(d);
}
quint32 DBODChunk::strideSize(const DGBLChunk *header, const DPELChunk *pel, const DLOCChunk *loc) const
{
auto width = loc ? loc->width() : header->width();
return (width * pel->depth() + 7) / 8;
}
/* ******************
* *** BEAM Chunk ***
* ****************** */

View File

@@ -55,6 +55,10 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
#define CAMG_CHUNK QByteArray("CAMG")
#define CMAP_CHUNK QByteArray("CMAP")
#define CMYK_CHUNK QByteArray("CMYK") // https://wiki.amigaos.net/wiki/ILBM_IFF_Interleaved_Bitmap#ILBM.CMYK
#define DBOD_CHUNK QByteArray("DBOD")
#define DGBL_CHUNK QByteArray("DGBL")
#define DLOC_CHUNK QByteArray("DLOC")
#define DPEL_CHUNK QByteArray("DPEL")
#define DPI__CHUNK QByteArray("DPI ")
#define IDAT_CHUNK QByteArray("IDAT")
#define IHDR_CHUNK QByteArray("IHDR")
@@ -65,6 +69,7 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
#define RFLG_CHUNK QByteArray("RFLG")
#define RGHD_CHUNK QByteArray("RGHD")
#define RSCM_CHUNK QByteArray("RSCM")
#define TVDC_CHUNK QByteArray("TVDC")
#define XBMI_CHUNK QByteArray("XBMI")
#define YUVS_CHUNK QByteArray("YUVS")
@@ -96,12 +101,14 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
// FORM types
#define ACBM_FORM_TYPE QByteArray("ACBM")
#define DEEP_FORM_TYPE QByteArray("DEEP")
#define ILBM_FORM_TYPE QByteArray("ILBM")
#define IMAG_FORM_TYPE QByteArray("IMAG")
#define PBM__FORM_TYPE QByteArray("PBM ")
#define RGB8_FORM_TYPE QByteArray("RGB8")
#define RGBN_FORM_TYPE QByteArray("RGBN")
#define RGFX_FORM_TYPE QByteArray("RGFX")
#define TVPP_FORM_TYPE QByteArray("TVPP") // same as DEEP
#define CIMG_FOR4_TYPE QByteArray("CIMG")
#define TBMP_FOR4_TYPE QByteArray("TBMP")
@@ -642,11 +649,23 @@ class CAMGChunk : public IFFChunk
{
public:
enum ModeId {
LoResLace = 0x0004,
GenLockVideo = 0x0002,
InterlacedDisplay = 0x0004,
DoubleScan = 0x0008,
SuperHighResolution = 0x0020,
PlayfieldBitplaneAdjust = 0x0040,
HalfBrite = 0x0080,
LoResDpf = 0x0400,
Ham = 0x0800,
HiRes = 0x8000
GenLockAudio = 0x0100,
DualPlayfield = 0x0400,
HoldAndModify = 0x0800,
ExtendedMode = 0x1000,
ViewPortHide = 0x2000,
Sprites = 0x4000,
HighResolution = 0x8000,
// aliases
Lace = InterlacedDisplay,
Ham = HoldAndModify
};
Q_DECLARE_FLAGS(ModeIds, ModeId)
@@ -946,6 +965,8 @@ private:
QImage::Format cdiFormat() const;
QImage::Format rgfxFormat() const;
QImage::Format deepFormat() const;
};
@@ -1946,6 +1967,266 @@ private:
};
/*!
* *** DEEP IFF CHUNKS ***
*/
/*!
* \brief The DGBLChunk class
*/
class DGBLChunk : public IFFChunk
{
public:
enum Compression : quint16 {
Uncompressed = 0,
Rle = 1,
Huffman = 2,
DynamicHuffman = 3,
Jpeg = 4,
TvDeepCompression = 5
};
virtual ~DGBLChunk() override;
DGBLChunk();
DGBLChunk(const DGBLChunk& other) = default;
DGBLChunk& operator =(const DGBLChunk& other) = default;
/*!
* \brief compression
* \return The type of compression used.
*/
Compression compression() const;
/*!
* \brief width
* \return Width of the source display in pixels.
*/
qint32 width() const;
/*!
* \brief height
* \return Height of the source display in pixels.
*/
qint32 height() const;
/*!
* \brief size
* \return Size of the source display in pixels.
*/
QSize size() const
{
return QSize(width(), height());
}
/*!
* \brief xAspectRatio
* \return X pixel aspect.
*/
quint8 xAspectRatio() const;
/*!
* \brief yAspectRatio
* \return Y pixel aspect.
*/
quint8 yAspectRatio() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(DGBL_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The DPELChunk class
*/
class DPELChunk : public IFFChunk
{
public:
enum DataType : quint16 {
Invalid = 0,
Red = 1,
Green = 2,
Blue = 3,
Alpha = 4,
Yellow = 5,
Cyan = 6,
Magenta = 7,
Black = 8,
Mask = 9,
ZBuffer = 10,
Opacity = 11,
LinearKey = 12,
BinaryKey = 13
};
struct Element {
Element(const DataType& t = DataType::Invalid, quint16 d = 0) : type(t), depth(d) {}
DataType type;
quint16 depth;
};
virtual ~DPELChunk() override;
DPELChunk();
DPELChunk(const DPELChunk& other) = default;
DPELChunk& operator =(const DPELChunk& other) = default;
/*!
* \brief count
* \return The number of elements
*/
qint32 count() const;
/*!
* \brief depth
* \return The pixel depth.
*/
qint32 depth() const;
/*!
* \brief elements
* Elements needed to identify the content of every pixel.
* Pixels will always be padded to byte boundaries.
* \return The list of elements. An empty list on error.
*/
QList<Element> elements() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(DPEL_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The DLOCChunk class
*/
class DLOCChunk : public IFFChunk
{
public:
virtual ~DLOCChunk() override;
DLOCChunk();
DLOCChunk(const DLOCChunk& other) = default;
DLOCChunk& operator =(const DLOCChunk& other) = default;
/*!
* \brief width
* \return Width of the bitmap in pixels.
*/
qint32 width() const;
/*!
* \brief height
* \return Height of the bitmap in pixels.
*/
qint32 height() const;
/*!
* \brief size
* \return Size in pixels.
*/
QSize size() const;
/*!
* \brief xOffset
* X offset of origin in source image.
*/
qint32 xOffset() const;
/*!
* \brief yOffset
* \returnX offset of origin in source image.
*/
qint32 yOffset() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(DLOC_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The TVDCChunk class
*/
class TVDCChunk : public IFFChunk
{
public:
virtual ~TVDCChunk() override;
TVDCChunk();
TVDCChunk(const TVDCChunk& other) = default;
TVDCChunk& operator =(const TVDCChunk& other) = default;
qint32 count() const;
QList<quint16> table() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(TVDC_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The DBODChunk class
*/
class DBODChunk : public IFFChunk
{
public:
virtual ~DBODChunk() override;
DBODChunk();
DBODChunk(const DBODChunk& other) = default;
DBODChunk& operator =(const DBODChunk& other) = default;
virtual bool isValid() const override;
CHUNKID_DEFINE(DBOD_CHUNK)
/*!
* \brief readStride
* \param d The device.
* \param y The current scanline.
* \param header The bitmap header.
* \param pel The pixel elements info.
* \param loc The display location (optional).
* \return The scanline as requested for QImage.
* \warning Call resetStrideRead() once before this one.
*/
QByteArray strideRead(QIODevice *d,
qint32 y,
const DGBLChunk *header,
const DPELChunk *pel,
const DLOCChunk *loc = nullptr,
const TVDCChunk *tvdc = nullptr) const;
/*!
* \brief resetStrideRead
* Reset the stride read set the position at the beginning of the data and reset all buffers.
* \param d The device.
* \return True on success, otherwise false.
* \sa strideRead
* \note Must be called once before strideRead().
*/
bool resetStrideRead(QIODevice *d) const;
protected:
/*!
* \brief strideSize
* \return The size of data to have to decode an image row.
*/
quint32 strideSize(const DGBLChunk *header, const DPELChunk *pel, const DLOCChunk *loc = nullptr) const;
};
/*!
* *** UNDOCUMENTED CHUNKS ***
*/

View File

@@ -1,4 +1,4 @@
{
"Keys": [ "dds" ],
"MimeTypes": [ "image/x-dds" ]
"Keys": [ "dds", "dds" ],
"MimeTypes": [ "image/vnd.ms-dds", "image/x-dds" ]
}

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>
@@ -208,6 +209,8 @@ EXRHandler::EXRHandler()
{
// Set the number of threads to use (0 is allowed)
Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
// Set maximum image size
Imf::Header::setMaxImageSize(EXR_MAX_IMAGE_WIDTH, EXR_MAX_IMAGE_HEIGHT);
}
bool EXRHandler::canRead() const
@@ -227,25 +230,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 +375,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 +426,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 +445,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 +735,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 +755,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 +830,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;

259
src/imageformats/ff.cpp Normal file
View File

@@ -0,0 +1,259 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
// Specs: https://tools.suckless.org/farbfeld/
#include "ff_p.h"
#include "util_p.h"
#include <QColorSpace>
#include <QIODevice>
#include <QImage>
#include <QLoggingCategory>
#include <QtEndian>
Q_DECLARE_LOGGING_CATEGORY(LOG_FFPLUGIN)
#ifdef QT_DEBUG
Q_LOGGING_CATEGORY(LOG_FFPLUGIN, "kf.imageformats.plugins.ff", QtDebugMsg)
#else
Q_LOGGING_CATEGORY(LOG_FFPLUGIN, "kf.imageformats.plugins.ff", QtWarningMsg)
#endif
/* *** FF_MAX_IMAGE_WIDTH and FF_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
#ifndef FF_MAX_IMAGE_WIDTH
#define FF_MAX_IMAGE_WIDTH KIF_LARGE_IMAGE_PIXEL_LIMIT
#endif
#ifndef FF_MAX_IMAGE_HEIGHT
#define FF_MAX_IMAGE_HEIGHT FF_MAX_IMAGE_WIDTH
#endif
#define HEADER_SIZE 16
class FFHeader
{
private:
QByteArray m_rawHeader;
public:
FFHeader()
{
}
bool isValid() const
{
if (m_rawHeader.size() < HEADER_SIZE) {
return false;
}
return m_rawHeader.startsWith(QByteArray::fromRawData("farbfeld", 8));
}
bool isSupported() const
{
auto w = width();
auto h = height();
if (w < 1 || w > FF_MAX_IMAGE_WIDTH || h < 1 || h > FF_MAX_IMAGE_HEIGHT) {
return false;
}
return format() != QImage::Format_Invalid;
}
qint32 width() const
{
if (!isValid()) {
return 0;
}
return qFromBigEndian<qint32>(m_rawHeader.data() + 8);
}
qint32 height() const
{
if (!isValid()) {
return 0;
}
return qFromBigEndian<qint32>(m_rawHeader.data() + 12);
}
QSize size() const
{
return QSize(width(), height());
}
QImage::Format format() const
{
if (!isValid()) {
return QImage::Format_Invalid;
}
return QImage::Format_RGBA64;
}
bool read(QIODevice *d)
{
m_rawHeader = d->read(HEADER_SIZE);
if (m_rawHeader.size() != HEADER_SIZE) {
return false;
}
return isValid();
}
bool peek(QIODevice *d)
{
m_rawHeader = d->peek(HEADER_SIZE);
if (m_rawHeader.size() != HEADER_SIZE) {
return false;
}
return isValid();
}
};
class FFHandlerPrivate
{
public:
FFHandlerPrivate() {}
~FFHandlerPrivate() {}
FFHeader m_header;
};
FFHandler::FFHandler()
: QImageIOHandler()
, d(new FFHandlerPrivate)
{
}
bool FFHandler::canRead() const
{
if (canRead(device())) {
setFormat("ff");
return true;
}
return false;
}
bool FFHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::canRead() called with no device";
return false;
}
FFHeader h;
if (!h.peek(device)) {
return false;
}
return h.isSupported();
}
bool FFHandler::read(QImage *image)
{
auto&& header = d->m_header;
if (!header.read(device())) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::read() invalid header";
return false;
}
auto img = imageAlloc(header.size(), header.format());
if (img.isNull()) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::read() error while allocating the image";
return false;
}
auto d = device();
auto size = img.bytesPerLine();
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
if (d->read(line, size) != size) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::read() error while reading image scanline";
return false;
}
#if Q_LITTLE_ENDIAN
for (auto i = 0; i < size; i += 2) {
std::swap(line[i], line[i + 1]);
}
#endif
}
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
*image = img;
return true;
}
bool FFHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
return false;
}
QVariant FFHandler::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 FFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "ff") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && FFHandler::canRead(device)) {
cap |= CanRead;
}
return cap;
}
QImageIOHandler *FFPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new FFHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_ff_p.cpp"

4
src/imageformats/ff.json Normal file
View File

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

42
src/imageformats/ff_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_FF_P_H
#define KIMG_FF_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class FFHandlerPrivate;
class FFHandler : public QImageIOHandler
{
public:
FFHandler();
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<FFHandlerPrivate> d;
};
class FFPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "ff.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_FF_P_H

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

@@ -11,6 +11,7 @@
#include "microexif_p.h"
#include "util_p.h"
#include <libheif/heif.h>
#include <libheif/heif_properties.h>
#include <QColorSpace>
#include <QLoggingCategory>
@@ -33,6 +34,28 @@ Q_LOGGING_CATEGORY(LOG_HEIFPLUGIN, "kf.imageformats.plugins.heif", QtWarningMsg)
#define HEIF_MAX_METADATA_SIZE (4 * 1024 * 1024)
#endif
#ifndef HEIF_DISABLE_QT_TRANSFORMATION
/*!
* HEIF transformations, in addition to rotations and reflections,
* also support image cropping. Consequently, the Qt plugin, must
* also honor the crop. This define is useful in case of problems:
* activating it disables Qt's support for transformations,
* delegating them to the HEIF libraries (which will therefore
* always apply them regardless of what is requested from Qt).
*/
// #define HEIF_DISABLE_QT_TRANSFORMATION
#endif
/* *** HEIF_MAX_IMAGE_WIDTH and HEIF_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
#ifndef HEIF_MAX_IMAGE_WIDTH
#define HEIF_MAX_IMAGE_WIDTH KIF_64K_IMAGE_PIXEL_LIMIT
#endif
#ifndef HEIF_MAX_IMAGE_HEIGHT
#define HEIF_MAX_IMAGE_HEIGHT HEIF_MAX_IMAGE_WIDTH
#endif
size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false;
@@ -72,6 +95,7 @@ static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx
HEIFHandler::HEIFHandler()
: m_parseState(ParseHeicNotParsed)
, m_quality(100)
, m_orientation(0)
{
}
@@ -123,15 +147,16 @@ bool HEIFHandler::write(const QImage &image)
return false;
}
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (image.width() >= HEIF_MAX_IMAGE_WIDTH || image.height() >= HEIF_MAX_IMAGE_HEIGHT) {
qCWarning(LOG_HEIFPLUGIN) << "Image size invalid:" << image.width() << "x" << image.height();
return false;
}
startHeifLib();
#endif
bool success = write_helper(image);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
finishHeifLib();
#endif
return success;
}
@@ -163,12 +188,10 @@ bool HEIFHandler::write_helper(const QImage &image)
}
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) {
@@ -325,12 +348,21 @@ bool HEIFHandler::write_helper(const QImage &image)
}
}
if (m_orientation >= 1 && m_orientation <= 8) {
// Function available from HEIF v1.14
encoder_options->image_orientation = heif_orientation(m_orientation);
}
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 (m_orientation >= 1 && m_orientation <= 8) {
// EXIF orientation must be coherent with HEIF orientation
exif.setOrientation(m_orientation);
}
if (!exif.isEmpty()) {
auto ba = exif.toByteArray();
err = heif_context_add_exif_metadata(context, handle, ba.constData(), ba.size());
@@ -376,6 +408,74 @@ bool HEIFHandler::write_helper(const QImage &image)
return true;
}
bool HEIFHandler::read_orientation_helper(void *heif_handle, const void *heif_ctx)
{
if (heif_handle == nullptr || heif_ctx == nullptr) {
return false;
}
auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
auto item_id = heif_image_handle_get_item_id(handle);
// get the properties
heif_transform_mirror_direction mirror = heif_transform_mirror_direction::heif_transform_mirror_direction_invalid;
heif_property_id mir_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_mirror, &mir_id, 1) > 0) {
mirror = heif_item_get_property_transform_mirror(ctx, item_id, mir_id);
if (mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid)
return false;
}
int rotation_ccw = -1;
heif_property_id rot_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_rotation, &rot_id, 1) > 0) {
rotation_ccw = heif_item_get_property_transform_rotation_ccw(ctx, item_id, rot_id);
if (rotation_ccw == -1)
return false;
}
if (rotation_ccw == -1 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 0;
} else if (rotation_ccw == 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 1;
} else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
m_orientation = 2;
} else if (rotation_ccw == 180 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 3;
} else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
m_orientation = 4;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
m_orientation = 5;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 6;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
m_orientation = 7;
} else if (rotation_ccw == 90 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 8;
}
return true;
}
bool HEIFHandler::read_crop(void *heif_handle, const void *heif_ctx, const QSize &size, QRect &crop)
{
if (heif_handle == nullptr || heif_ctx == nullptr) {
return false;
}
auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
auto item_id = heif_image_handle_get_item_id(handle);
heif_property_id crop_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_crop, &crop_id, 1) > 0) {
int l = 0, t = 0, r = 0, b = 0;
heif_item_get_property_transform_crop_borders(ctx, item_id, crop_id, size.width(), size.height(), &l, &t, &r, &b);
crop = QRect(QPoint(t, l), size - QSize(b + t, r + l));
}
return crop.isValid();
}
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
{
if (header.size() < 28) {
@@ -456,10 +556,10 @@ QVariant HEIFHandler::option(ImageOption option) const
switch (option) {
case Size:
return m_current_image.size();
break;
case ImageTransformation:
return int(MicroExif::orientationToTransformation(m_orientation));
default:
return QVariant();
break;
}
}
@@ -474,6 +574,9 @@ void HEIFHandler::setOption(ImageOption option, const QVariant &value)
m_quality = 100;
}
break;
case ImageTransformation:
m_orientation = MicroExif::transformationToOrientation(QImageIOHandler::Transformation(value.toUInt()));
break;
default:
QImageIOHandler::setOption(option, value);
break;
@@ -482,7 +585,11 @@ void HEIFHandler::setOption(ImageOption option, const QVariant &value)
bool HEIFHandler::supportsOption(ImageOption option) const
{
return option == Quality || option == Size;
auto ok = option == Quality || option == Size;
#ifndef HEIF_DISABLE_QT_TRANSFORMATION
ok = ok || option == ImageTransformation;
#endif
return ok;
}
bool HEIFHandler::ensureParsed() const
@@ -496,15 +603,12 @@ bool HEIFHandler::ensureParsed() const
HEIFHandler *that = const_cast<HEIFHandler *>(this);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
startHeifLib();
#endif
bool success = that->ensureDecoder();
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
finishHeifLib();
#endif
return success;
}
@@ -589,16 +693,19 @@ bool HEIFHandler::ensureDecoder()
return false;
}
bool ignore_transformations = false;
struct heif_decoding_options *decoder_option = heif_decoding_options_alloc();
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
decoder_option->strict_decoding = 1;
#ifdef HEIF_DISABLE_QT_TRANSFORMATION
decoder_option->ignore_transformations = ignore_transformations;
#else
decoder_option->ignore_transformations = ignore_transformations = read_orientation_helper(handle, ctx);
#endif
struct heif_image *img = nullptr;
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr && buffer.contains("Xiaomi")) {
qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!";
@@ -606,7 +713,6 @@ bool HEIFHandler::ensureDecoder()
decoder_option->strict_decoding = 0;
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
}
#endif
if (decoder_option) {
heif_decoding_options_free(decoder_option);
@@ -623,14 +729,17 @@ bool HEIFHandler::ensureDecoder()
const int imageWidth = heif_image_get_width(img, heif_channel_interleaved);
const int imageHeight = heif_image_get_height(img, heif_channel_interleaved);
QSize imageSize(imageWidth, imageHeight);
QSize imageSize;
if (imageWidth < HEIF_MAX_IMAGE_WIDTH && imageHeight < HEIF_MAX_IMAGE_HEIGHT) {
imageSize = QSize(imageWidth, imageHeight);
}
if (!imageSize.isValid()) {
heif_image_release(img);
heif_image_handle_release(handle);
heif_context_free(ctx);
m_parseState = ParseHeicError;
qCWarning(LOG_HEIFPLUGIN) << "HEIC image size invalid:" << imageSize;
qCWarning(LOG_HEIFPLUGIN) << "HEIC image size invalid:" << imageWidth << "x" << imageHeight;
return false;
}
@@ -852,6 +961,12 @@ bool HEIFHandler::ensureDecoder()
break;
}
if (ignore_transformations) {
QRect crop_rect;
if (read_crop(handle, ctx, m_current_image.size(), crop_rect))
m_current_image = m_current_image.copy(crop_rect);
}
heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle);
if (profileType == heif_color_profile_type_prof || profileType == heif_color_profile_type_rICC) {
size_t rawProfileSize = heif_image_handle_get_raw_color_profile_size(handle);
@@ -1045,34 +1160,27 @@ void HEIFHandler::queryHeifLib()
QMutexLocker locker(&getHEIFHandlerMutex());
if (!m_plugins_queried) {
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_init(nullptr);
}
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
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, 6)
m_avci_decoder_available = heif_have_decoder_for_format(heif_compression_AVC);
#endif
m_plugins_queried = true;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
}
void HEIFHandler::startHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
QMutexLocker locker(&getHEIFHandlerMutex());
if (m_initialized_count == 0) {
@@ -1080,12 +1188,10 @@ void HEIFHandler::startHeifLib()
}
m_initialized_count++;
#endif
}
void HEIFHandler::finishHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
QMutexLocker locker(&getHEIFHandlerMutex());
if (m_initialized_count == 0) {
@@ -1096,8 +1202,6 @@ void HEIFHandler::finishHeifLib()
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
QMutex &HEIFHandler::getHEIFHandlerMutex()

View File

@@ -51,9 +51,24 @@ private:
ParseHeicState m_parseState;
int m_quality;
QImage m_current_image;
quint16 m_orientation;
bool write_helper(const QImage &image);
/*!
* \brief heif_orientation_helper
* Read the transform_mirror and transform_rotation_ccw properties and set \a m_orientation
* \return True on success, otherwise false.
*/
bool read_orientation_helper(void *heif_handle, const void *heif_ctx);
/*!
* \brief read_crop
* Read the crop information.
* \return True on success, otherwise false.
*/
bool read_crop(void *heif_handle, const void *heif_ctx, const QSize& size, QRect &crop);
static void startHeifLib();
static void finishHeifLib();
static void queryHeifLib();

View File

@@ -607,6 +607,69 @@ bool IFFHandler::readRGFXImage(QImage *image)
return true;
}
bool IFFHandler::readDEEPImage(QImage *image)
{
auto forms = d->searchForms<FORMChunk>();
if (forms.isEmpty()) {
return false;
}
auto cin = qBound(0, currentImageNumber(), int(forms.size() - 1));
auto &&form = forms.at(cin);
// show the first one (I don't have a sample with many images)
auto headers = IFFChunk::searchT<DGBLChunk>(form);
if (headers.isEmpty()) {
return false;
}
// create the image
auto &&header = headers.first();
auto size = header->size();
auto locs = IFFChunk::searchT<DLOCChunk>(form);
if (!locs.isEmpty()) {
size = locs.first()->size();
}
auto img = imageAlloc(size, form->format());
if (img.isNull()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readDEEPImage(): error while allocating the image";
return false;
}
// decoding the image
auto bodies = IFFChunk::searchT<DBODChunk>(form);
if (bodies.isEmpty()) {
img.fill(0);
} else {
auto &&body = bodies.first();
if (!body->resetStrideRead(device())) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readDEEPImage(): error while reading image data";
return false;
}
auto pels = IFFChunk::searchT<DPELChunk>(form);
if (pels.isEmpty()) {
// DPEL is used to calculate the image format, so here should not be empty.
return false;
}
auto tvdcs = IFFChunk::searchT<TVDCChunk>(form);
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
auto ba = body->strideRead(device(), y, header, pels.first(),
locs.isEmpty() ? nullptr : locs.first(),
tvdcs.isEmpty() ? nullptr : tvdcs.first());
if (ba.isEmpty()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readDEEPImage(): error while reading image scanline";
return false;
}
memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size()));
}
}
// set metadata (including image resolution)
addMetadata(img, form);
*image = img;
return true;
}
bool IFFHandler::read(QImage *image)
{
if (!d->readStructure(device())) {
@@ -630,6 +693,10 @@ bool IFFHandler::read(QImage *image)
return true;
}
if (readDEEPImage(image)) {
return true;
}
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): no supported image found";
return false;
}

View File

@@ -39,6 +39,8 @@ private:
bool readRGFXImage(QImage *image);
bool readDEEPImage(QImage *image);
private:
const QScopedPointer<IFFHandlerPrivate> d;
};

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()
{
@@ -926,7 +996,7 @@ private:
return true;
}
DESCRIPTIVEMETADATA meta;
DESCRIPTIVEMETADATA meta = {};
if (pDecoder->GetDescriptiveMetadata(pDecoder, &meta)) {
return false;
}
@@ -961,13 +1031,13 @@ bool JXRHandler::read(QImage *outImage)
PKPixelFormatGUID convFmt;
auto imageFmt = d->imageFormat(&convFmt);
auto img = imageAlloc(d->imageSize(), imageFmt);
auto img = imageAlloc(d->imageSize(), imageFmt, ImageInitToZero::FPOnly);
if (img.isNull()) {
return false;
}
// 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>
@@ -722,7 +744,42 @@ void MicroExif::setOrientation(quint16 orient)
QImageIOHandler::Transformation MicroExif::transformation() const
{
switch (orientation()) {
return orientationToTransformation(orientation());
}
void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
{
setOrientation(transformationToOrientation(t));
}
quint16 MicroExif::transformationToOrientation(const QImageIOHandler::Transformation &t)
{
switch (t) {
case QImageIOHandler::TransformationNone:
return 1;
case QImageIOHandler::TransformationMirror:
return 2;
case QImageIOHandler::TransformationRotate180:
return 3;
case QImageIOHandler::TransformationFlip:
return 4;
case QImageIOHandler::TransformationFlipAndRotate90:
return 5;
case QImageIOHandler::TransformationRotate90:
return 6;
case QImageIOHandler::TransformationMirrorAndRotate90:
return 7;
case QImageIOHandler::TransformationRotate270:
return 8;
default:
break;
}
return 0; // no orientation set
}
QImageIOHandler::Transformation MicroExif::orientationToTransformation(quint16 o)
{
switch (o) {
case 1:
return QImageIOHandler::TransformationNone;
case 2:
@@ -745,39 +802,6 @@ QImageIOHandler::Transformation MicroExif::transformation() const
return QImageIOHandler::TransformationNone;
}
void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
{
switch (t) {
case QImageIOHandler::TransformationNone:
setOrientation(1);
break;
case QImageIOHandler::TransformationMirror:
setOrientation(2);
break;
case QImageIOHandler::TransformationRotate180:
setOrientation(3);
break;
case QImageIOHandler::TransformationFlip:
setOrientation(4);
break;
case QImageIOHandler::TransformationFlipAndRotate90:
setOrientation(5);
break;
case QImageIOHandler::TransformationRotate90:
setOrientation(6);
break;
case QImageIOHandler::TransformationMirrorAndRotate90:
setOrientation(7);
break;
case QImageIOHandler::TransformationRotate270:
setOrientation(8);
break;
default:
break;
}
setOrientation(0); // no orientation set
}
QString MicroExif::software() const
{
return tiffString(TIFF_SOFTWARE);
@@ -884,7 +908,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 +928,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 +948,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 +990,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 +1201,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 +1371,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 +1449,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 +1540,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 +1554,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,
@@ -131,6 +210,19 @@ public:
QImageIOHandler::Transformation transformation() const;
void setTransformation(const QImageIOHandler::Transformation& t);
/*!
* \brief transformationToOrientation
* \param t The Qt transformation.
* \return The EXIF orientation value or 0 if none.
*/
static quint16 transformationToOrientation(const QImageIOHandler::Transformation& t);
/*!
* \brief orientationToTransformation
* \param o The EXIF orientation.
* \return The orientation converted in the equivalent Qt transformation.
*/
static QImageIOHandler::Transformation orientationToTransformation(quint16 o);
/*!
* \brief software
* \return Name and version number of the software package(s) used to create the image.
@@ -236,6 +328,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 +413,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

@@ -65,11 +65,11 @@ public:
inline int width() const
{
return (XMax - XMin) + 1;
return int(XMax - XMin) + 1;
}
inline int height() const
{
return (YMax - YMin) + 1;
return int(YMax - YMin) + 1;
}
inline bool isCompressed() const
{

View File

@@ -35,7 +35,7 @@
* The maximum size in pixel allowed by the plugin.
*/
#ifndef RAW_MAX_IMAGE_WIDTH
#define RAW_MAX_IMAGE_WIDTH std::min(65535, KIF_LARGE_IMAGE_PIXEL_LIMIT)
#define RAW_MAX_IMAGE_WIDTH KIF_64K_IMAGE_PIXEL_LIMIT
#endif
#ifndef RAW_MAX_IMAGE_HEIGHT
#define RAW_MAX_IMAGE_HEIGHT RAW_MAX_IMAGE_WIDTH
@@ -759,8 +759,8 @@ bool LoadRAW(QImageIOHandler *handler, QImage &img)
// *** Limiting the maximum image size on a reasonable size
auto size = rawImageSize(rawProcessor.get());
if (size.width() > RAW_MAX_IMAGE_WIDTH || size.height() > RAW_MAX_IMAGE_HEIGHT) {
qCWarning(LOG_RAWPLUGIN) << "The maximum image size is limited to" << RAW_MAX_IMAGE_WIDTH << "x" << RAW_MAX_IMAGE_HEIGHT << "px";
if (size.width() >= RAW_MAX_IMAGE_WIDTH || size.height() >= RAW_MAX_IMAGE_HEIGHT) {
qCWarning(LOG_RAWPLUGIN) << "The maximum image size is limited to" << (RAW_MAX_IMAGE_WIDTH - 1) << "x" << (RAW_MAX_IMAGE_HEIGHT - 1) << "px";
return false;
}

View File

@@ -13,12 +13,21 @@
#include <QImage>
#include <QImageIOHandler>
#include <QIODevice>
#include <QPixelFormat>
// Default maximum width and height for the large image plugins.
#ifndef KIF_LARGE_IMAGE_PIXEL_LIMIT
#define KIF_LARGE_IMAGE_PIXEL_LIMIT 300000
#endif
// Maximum size for legacy image formats.
#define KIF_64K_IMAGE_PIXEL_LIMIT 65536
#if KIF_LARGE_IMAGE_PIXEL_LIMIT < KIF_64K_IMAGE_PIXEL_LIMIT
#undef KIF_LARGE_IMAGE_PIXEL_LIMIT
#define KIF_LARGE_IMAGE_PIXEL_LIMIT KIF_64K_IMAGE_PIXEL_LIMIT
#endif
// Image metadata keys to use in plugins (so they are consistent)
#define META_KEY_ALTITUDE "Altitude"
#define META_KEY_AUTHOR "Author"
@@ -34,10 +43,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"
@@ -51,20 +72,50 @@
// QList uses some extra space for stuff, hence the 32 here suggested by Thiago Macieira
static constexpr int kMaxQVectorSize = std::numeric_limits<int>::max() - 32;
// On Qt 6 to make the plugins fail to allocate if the image size is greater than QImageReader::allocationLimit()
// it is necessary to allocate the image with QImageIOHandler::allocateImage().
inline QImage imageAlloc(const QSize &size, const QImage::Format &format)
/*!
* \brief The ImageInitToZero enum
*/
enum class ImageInitToZero
{
None, /**< Do not initialize the image. */
All, /**< Initialize all images. */
PremulOnly, /**< Initialize only images with premultiplied alpha. */
FPOnly, /**< Initialize only images with floating point format. */
FPAndPremul /**< Initialize floating point and premultiplied formats. */
};
/*!
* \brief imageAlloc
* Helper function to initialize framework images.
* \param size The image size.
* \param format The image format,
* \param init Whether and which images should be initialized to zero.
* \return The allocated image or a null image on error.
*/
inline QImage imageAlloc(const QSize &size, const QImage::Format &format, const ImageInitToZero& init = ImageInitToZero::None)
{
QImage img;
if (!QImageIOHandler::allocateImage(size, format, &img)) {
img = QImage(); // paranoia
}
if (init != ImageInitToZero::None && !img.isNull()) {
auto pixelFormat = img.pixelFormat();
auto isFloat = pixelFormat.typeInterpretation() == QPixelFormat::FloatingPoint;
auto isPremul = pixelFormat.premultiplied();
if (init == ImageInitToZero::All) {
img.fill(0);
} else if (isFloat && (init == ImageInitToZero::FPOnly || init == ImageInitToZero::FPAndPremul)) {
img.fill(0);
} else if (isPremul && (init == ImageInitToZero::PremulOnly || init == ImageInitToZero::FPAndPremul)) {
img.fill(0);
}
}
return img;
}
inline QImage imageAlloc(qint32 width, qint32 height, const QImage::Format &format)
inline QImage imageAlloc(qint32 width, qint32 height, const QImage::Format &format, const ImageInitToZero& init = ImageInitToZero::None)
{
return imageAlloc(QSize(width, height), format);
return imageAlloc(QSize(width, height), format, init);
}
template<class TI, class SF> // SF = source FP, TI = target INT