mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-05-28 00:30:23 -04:00
exr: write support and more
- Added support for writing EXR files - Large image support (tested with 32GiB 16-bit image) - OpenEXR multithreaded read/write support through the use of line blocks - Build option to enable conversion to sRGB when read (disabled by default) - Multi-view image reading support - Options support: Size, ImageFormat, Quality, CompressRatio - Metadata write/read support via EXR attributes The plugin is now quite complete. It should also work on KF5 (not tested): if there is the will to include this MR also on KF5, let me know and I will do all the necessary tests.
This commit is contained in:
parent
6a51407556
commit
7899c27a80
@ -17,7 +17,6 @@ The following image formats have read-only support:
|
|||||||
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
|
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
|
||||||
- Gimp (xcf)
|
- Gimp (xcf)
|
||||||
- Krita (kra)
|
- Krita (kra)
|
||||||
- OpenEXR (exr)
|
|
||||||
- OpenRaster (ora)
|
- OpenRaster (ora)
|
||||||
- Photoshop documents (psd, psb, pdd, psdt)
|
- Photoshop documents (psd, psb, pdd, psdt)
|
||||||
- Radiance HDR (hdr)
|
- Radiance HDR (hdr)
|
||||||
@ -29,6 +28,7 @@ The following image formats have read and write support:
|
|||||||
- Encapsulated PostScript (eps)
|
- Encapsulated PostScript (eps)
|
||||||
- High Efficiency Image File Format (heif). Can be enabled with the KIMAGEFORMATS_HEIF build option.
|
- High Efficiency Image File Format (heif). Can be enabled with the KIMAGEFORMATS_HEIF build option.
|
||||||
- JPEG XL (jxl)
|
- JPEG XL (jxl)
|
||||||
|
- OpenEXR (exr)
|
||||||
- Personal Computer Exchange (pcx)
|
- Personal Computer Exchange (pcx)
|
||||||
- Quite OK Image format (qoi)
|
- Quite OK Image format (qoi)
|
||||||
- SGI images (rgb, rgba, sgi, bw)
|
- SGI images (rgb, rgba, sgi, bw)
|
||||||
@ -45,10 +45,6 @@ of Qt is the license. As such, if you write an imageformat plugin and
|
|||||||
you are willing to sign the Qt Project contributor agreement, it may be
|
you are willing to sign the Qt Project contributor agreement, it may be
|
||||||
better to submit the plugin directly to the Qt Project.
|
better to submit the plugin directly to the Qt Project.
|
||||||
|
|
||||||
Note that the imageformat plugins provided by this module also provide a
|
|
||||||
desktop file. This is for the benefit of KImageIO in the KDE4 Support
|
|
||||||
framework.
|
|
||||||
|
|
||||||
## Duplicated Plugins
|
## Duplicated Plugins
|
||||||
|
|
||||||
The TGA plugin supports more formats than Qt's own TGA plugin;
|
The TGA plugin supports more formats than Qt's own TGA plugin;
|
||||||
|
@ -138,6 +138,11 @@ if (OpenEXR_FOUND)
|
|||||||
kimageformats_read_tests(
|
kimageformats_read_tests(
|
||||||
exr
|
exr
|
||||||
)
|
)
|
||||||
|
# Color space conversions from sRGB to linear on saving and
|
||||||
|
# from linear to sRGB on loading result in some rounding errors.
|
||||||
|
kimageformats_write_tests(FUZZ 5
|
||||||
|
exr-nodatacheck-lossless
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (LibRaw_FOUND)
|
if (LibRaw_FOUND)
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.6 KiB |
@ -7,6 +7,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
|
#include <QColorSpace>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@ -23,8 +24,8 @@ int main(int argc, char **argv)
|
|||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
QCoreApplication::removeLibraryPath(QStringLiteral(PLUGIN_DIR));
|
QCoreApplication::removeLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||||
QCoreApplication::addLibraryPath(QStringLiteral(PLUGIN_DIR));
|
QCoreApplication::addLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("readtest"));
|
QCoreApplication::setApplicationName(QStringLiteral("writetest"));
|
||||||
QCoreApplication::setApplicationVersion(QStringLiteral("1.0.0"));
|
QCoreApplication::setApplicationVersion(QStringLiteral("1.1.0"));
|
||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(QStringLiteral("Performs basic image conversion checking."));
|
parser.setApplicationDescription(QStringLiteral("Performs basic image conversion checking."));
|
||||||
@ -148,6 +149,14 @@ int main(int argc, char **argv)
|
|||||||
++failed;
|
++failed;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (reReadImage.colorSpace().isValid()) {
|
||||||
|
QColorSpace toColorSpace;
|
||||||
|
if (pngImage.colorSpace().isValid()) {
|
||||||
|
reReadImage.convertToColorSpace(pngImage.colorSpace());
|
||||||
|
} else {
|
||||||
|
reReadImage.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||||
|
}
|
||||||
|
}
|
||||||
reReadImage = reReadImage.convertToFormat(pngImage.format());
|
reReadImage = reReadImage.convertToFormat(pngImage.format());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ endif()
|
|||||||
##################################
|
##################################
|
||||||
|
|
||||||
if(OpenEXR_FOUND)
|
if(OpenEXR_FOUND)
|
||||||
kimageformats_add_plugin(kimg_exr SOURCES exr.cpp)
|
kimageformats_add_plugin(kimg_exr SOURCES exr.cpp scanlineconverter.cpp)
|
||||||
if(TARGET OpenEXR::OpenEXR)
|
if(TARGET OpenEXR::OpenEXR)
|
||||||
target_link_libraries(kimg_exr PRIVATE OpenEXR::OpenEXR)
|
target_link_libraries(kimg_exr PRIVATE OpenEXR::OpenEXR)
|
||||||
else()
|
else()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
KImageIO Routines to read (and perhaps in the future, write) images
|
The high dynamic range EXR format support for QImage.
|
||||||
in the high dynamic range EXR format.
|
|
||||||
|
|
||||||
SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
|
SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
|
||||||
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||||
@ -16,15 +15,52 @@
|
|||||||
*/
|
*/
|
||||||
//#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file
|
//#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file
|
||||||
|
|
||||||
/* *** EXR_ALLOW_LINEAR_COLORSPACE ***
|
/* *** EXR_CONVERT_TO_SRGB ***
|
||||||
* If defined, the linear data is kept and it is the display program that
|
* If defined, the linear data is converted to sRGB on read to accommodate
|
||||||
* must convert to the monitor profile. Otherwise the data is converted to sRGB
|
* programs that do not support color profiles.
|
||||||
* to accommodate programs that do not support color profiles.
|
* Otherwise the data are kept as is and it is the display program that
|
||||||
|
* must convert to the monitor profile.
|
||||||
* NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored.
|
* NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored.
|
||||||
*/
|
*/
|
||||||
//#define EXR_ALLOW_LINEAR_COLORSPACE // default: commented -> you should define it in your cmake file
|
//#define EXR_CONVERT_TO_SRGB // default: commented -> you should define it in your cmake file
|
||||||
|
|
||||||
|
/* *** EXR_STORE_XMP_ATTRIBUTE ***
|
||||||
|
* If defined, disables the stores XMP values in a non-standard attribute named "xmp".
|
||||||
|
* The QImage metadata used is "XML:com.adobe.xmp".
|
||||||
|
* NOTE: The use of non-standard attributes is possible but discouraged by the specification. However,
|
||||||
|
* metadata is essential for good image management and programs like darktable also set this
|
||||||
|
* attribute. Gimp reads the "xmp" attribute and Darktable writes it as well.
|
||||||
|
*/
|
||||||
|
//#define EXR_DISABLE_XMP_ATTRIBUTE // default: commented -> you should define it in your cmake file
|
||||||
|
|
||||||
|
/* *** EXR_MAX_IMAGE_WIDTH and EXR_MAX_IMAGE_HEIGHT ***
|
||||||
|
* The maximum size in pixel allowed by the plugin.
|
||||||
|
*/
|
||||||
|
#ifndef EXR_MAX_IMAGE_WIDTH
|
||||||
|
#define EXR_MAX_IMAGE_WIDTH 300000
|
||||||
|
#endif
|
||||||
|
#ifndef EXR_MAX_IMAGE_HEIGHT
|
||||||
|
#define EXR_MAX_IMAGE_HEIGHT EXR_MAX_IMAGE_WIDTH
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* *** EXR_LINES_PER_BLOCK ***
|
||||||
|
* Allows certain compression schemes to work in multithreading
|
||||||
|
* Requires up to "LINES_PER_BLOCK * MAX_IMAGE_WIDTH * 8"
|
||||||
|
* additional RAM (e.g. if 128, up to 307MiB of RAM).
|
||||||
|
* There is a performance gain with the following parameters (based on empirical tests):
|
||||||
|
* - PIZ compression needs 64+ lines
|
||||||
|
* - ZIPS compression needs 8+ lines
|
||||||
|
* - ZIP compression needs 32+ lines
|
||||||
|
* - Others not tested
|
||||||
|
*
|
||||||
|
* NOTE: The OpenEXR documentation states that the higher the better :)
|
||||||
|
*/
|
||||||
|
#ifndef EXR_LINES_PER_BLOCK
|
||||||
|
#define EXR_LINES_PER_BLOCK 128
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "exr_p.h"
|
#include "exr_p.h"
|
||||||
|
#include "scanlineconverter_p.h"
|
||||||
#include "util_p.h"
|
#include "util_p.h"
|
||||||
|
|
||||||
#include <IexThrowErrnoExc.h>
|
#include <IexThrowErrnoExc.h>
|
||||||
@ -39,10 +75,9 @@
|
|||||||
#include <ImfInt64.h>
|
#include <ImfInt64.h>
|
||||||
#include <ImfIntAttribute.h>
|
#include <ImfIntAttribute.h>
|
||||||
#include <ImfLineOrderAttribute.h>
|
#include <ImfLineOrderAttribute.h>
|
||||||
|
#include <ImfPreviewImage.h>
|
||||||
#include <ImfRgbaFile.h>
|
#include <ImfRgbaFile.h>
|
||||||
#include <ImfStandardAttributes.h>
|
#include <ImfStandardAttributes.h>
|
||||||
#include <ImfStringAttribute.h>
|
|
||||||
#include <ImfVecAttribute.h>
|
|
||||||
#include <ImfVersion.h>
|
#include <ImfVersion.h>
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@ -53,8 +88,12 @@
|
|||||||
#include <QFloat16>
|
#include <QFloat16>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QImageIOPlugin>
|
#include <QImageIOPlugin>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||||
#include <QTimeZone>
|
#include <QTimeZone>
|
||||||
|
#endif
|
||||||
|
|
||||||
// Allow the code to works on all QT versions supported by KDE
|
// Allow the code to works on all QT versions supported by KDE
|
||||||
// project (Qt 5.15 and Qt 6.x) to easy backports fixes.
|
// project (Qt 5.15 and Qt 6.x) to easy backports fixes.
|
||||||
@ -122,6 +161,57 @@ void K_IStream::clear()
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class K_OStream : public Imf::OStream
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
K_OStream(QIODevice *dev, const QByteArray &fileName)
|
||||||
|
: OStream(fileName.data())
|
||||||
|
, m_dev(dev)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(const char c[/*n*/], int n) override;
|
||||||
|
#if OPENEXR_VERSION_MAJOR > 2
|
||||||
|
uint64_t tellp() override;
|
||||||
|
void seekp(uint64_t pos) override;
|
||||||
|
#else
|
||||||
|
Imf::Int64 tellp() override;
|
||||||
|
void seekp(Imf::Int64 pos) override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private:
|
||||||
|
QIODevice *m_dev;
|
||||||
|
};
|
||||||
|
|
||||||
|
void K_OStream::write(const char c[], int n)
|
||||||
|
{
|
||||||
|
qint64 result = m_dev->write(c, n);
|
||||||
|
if (result > 0) {
|
||||||
|
return;
|
||||||
|
} else { // negative value {
|
||||||
|
Iex::throwErrnoExc("Error in write", result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if OPENEXR_VERSION_MAJOR > 2
|
||||||
|
uint64_t K_OStream::tellp()
|
||||||
|
#else
|
||||||
|
Imf::Int64 K_OStream::tellg()
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
return m_dev->pos();
|
||||||
|
}
|
||||||
|
|
||||||
|
#if OPENEXR_VERSION_MAJOR > 2
|
||||||
|
void K_OStream::seekp(uint64_t pos)
|
||||||
|
#else
|
||||||
|
void K_OStream::seekg(Imf::Int64 pos)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
m_dev->seek(pos);
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef EXR_USE_LEGACY_CONVERSIONS
|
#ifdef EXR_USE_LEGACY_CONVERSIONS
|
||||||
// source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html
|
// source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html
|
||||||
inline unsigned char gamma(float x)
|
inline unsigned char gamma(float x)
|
||||||
@ -139,7 +229,14 @@ inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
EXRHandler::EXRHandler()
|
EXRHandler::EXRHandler()
|
||||||
|
: m_compressionRatio(-1)
|
||||||
|
, m_quality(-1)
|
||||||
|
, m_imageNumber(-1)
|
||||||
|
, m_imageCount(0)
|
||||||
|
, m_startPos(-1)
|
||||||
{
|
{
|
||||||
|
// Set the number of threads to use (0 is allowed)
|
||||||
|
Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EXRHandler::canRead() const
|
bool EXRHandler::canRead() const
|
||||||
@ -151,113 +248,571 @@ bool EXRHandler::canRead() const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
|
||||||
|
{
|
||||||
|
auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
|
||||||
|
#if defined(EXR_USE_LEGACY_CONVERSIONS)
|
||||||
|
return (isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||||
|
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
|
||||||
|
return (isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
|
||||||
|
#else
|
||||||
|
return (isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief viewList
|
||||||
|
* \param header
|
||||||
|
* \return The list of available views.
|
||||||
|
*/
|
||||||
|
static QStringList viewList(const Imf::Header &h)
|
||||||
|
{
|
||||||
|
QStringList l;
|
||||||
|
if (auto views = h.findTypedAttribute<Imf::StringVectorAttribute>("multiView")) {
|
||||||
|
for (auto &&v : views->value()) {
|
||||||
|
l << QString::fromStdString(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
void printAttributes(const Imf::Header &h)
|
||||||
|
{
|
||||||
|
for (auto i = h.begin(); i != h.end(); ++i) {
|
||||||
|
qDebug() << i.name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief readMetadata
|
||||||
|
* Reads EXR attributes from the \a header and set its as metadata in the \a image.
|
||||||
|
*/
|
||||||
|
static void readMetadata(const Imf::Header &header, QImage &image)
|
||||||
|
{
|
||||||
|
// set some useful metadata
|
||||||
|
if (auto comments = header.findTypedAttribute<Imf::StringAttribute>("comments")) {
|
||||||
|
image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto owner = header.findTypedAttribute<Imf::StringAttribute>("owner")) {
|
||||||
|
image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto lat = header.findTypedAttribute<Imf::FloatAttribute>("latitude")) {
|
||||||
|
image.setText(QStringLiteral("Latitude"), QLocale::c().toString(lat->value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto lon = header.findTypedAttribute<Imf::FloatAttribute>("longitude")) {
|
||||||
|
image.setText(QStringLiteral("Longitude"), QLocale::c().toString(lon->value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto alt = header.findTypedAttribute<Imf::FloatAttribute>("altitude")) {
|
||||||
|
image.setText(QStringLiteral("Altitude"), QLocale::c().toString(alt->value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto capDate = header.findTypedAttribute<Imf::StringAttribute>("capDate")) {
|
||||||
|
float off = 0;
|
||||||
|
if (auto utcOffset = header.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) {
|
||||||
|
off = utcOffset->value();
|
||||||
|
}
|
||||||
|
auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
|
||||||
|
if (dateTime.isValid()) {
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||||
|
dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
|
||||||
|
#else
|
||||||
|
dateTime.setOffsetFromUtc(off);
|
||||||
|
#endif
|
||||||
|
image.setText(QStringLiteral("CreationDate"), dateTime.toString(Qt::ISODate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto xDensity = header.findTypedAttribute<Imf::FloatAttribute>("xDensity")) {
|
||||||
|
float par = 1;
|
||||||
|
if (auto pixelAspectRatio = header.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) {
|
||||||
|
par = pixelAspectRatio->value();
|
||||||
|
}
|
||||||
|
image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54));
|
||||||
|
image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-standard attribute
|
||||||
|
if (auto xmp = header.findTypedAttribute<Imf::StringAttribute>("xmp")) {
|
||||||
|
image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromStdString(xmp->value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: OpenEXR 3.2 metadata
|
||||||
|
*
|
||||||
|
* New Optional Standard Attributes:
|
||||||
|
* - Support automated editorial workflow:
|
||||||
|
* reelName, imageCounter, ascFramingDecisionList
|
||||||
|
*
|
||||||
|
* - Support forensics (“which other shots used that camera and lens before the camera firmware was updated?”):
|
||||||
|
* cameraMake, cameraModel, cameraSerialNumber, cameraFirmware, cameraUuid, cameraLabel, lensMake, lensModel,
|
||||||
|
* lensSerialNumber, lensFirmware, cameraColorBalance
|
||||||
|
*
|
||||||
|
* -Support pickup shots (reproduce critical camera settings):
|
||||||
|
* shutterAngle, cameraCCTSetting, cameraTintSetting
|
||||||
|
*
|
||||||
|
* - Support metadata-driven match move:
|
||||||
|
* sensorCenterOffset, sensorOverallDimensions, sensorPhotositePitch, sensorAcquisitionRectanglenominalFocalLength,
|
||||||
|
* effectiveFocalLength, pinholeFocalLength, entrancePupilOffset, tStop(complementing existing 'aperture')
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief readColorSpace
|
||||||
|
* Reads EXR chromaticities from the \a header and set its as color profile in the \a image.
|
||||||
|
*/
|
||||||
|
static void readColorSpace(const Imf::Header &header, QImage &image)
|
||||||
|
{
|
||||||
|
// final color operations
|
||||||
|
#ifndef EXR_USE_LEGACY_CONVERSIONS
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (!cs.isValid()) {
|
||||||
|
cs = QColorSpace(QColorSpace::SRgbLinear);
|
||||||
|
}
|
||||||
|
image.setColorSpace(cs);
|
||||||
|
|
||||||
|
#ifdef EXR_CONVERT_TO_SRGB
|
||||||
|
image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // !EXR_USE_LEGACY_CONVERSIONS
|
||||||
|
}
|
||||||
|
|
||||||
bool EXRHandler::read(QImage *outImage)
|
bool EXRHandler::read(QImage *outImage)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
int width;
|
auto d = device();
|
||||||
int height;
|
|
||||||
|
|
||||||
K_IStream istr(device(), QByteArray());
|
// set the image position after the first run.
|
||||||
|
if (!d->isSequential()) {
|
||||||
|
if (m_startPos < 0) {
|
||||||
|
m_startPos = d->pos();
|
||||||
|
} else {
|
||||||
|
d->seek(m_startPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
K_IStream istr(d, QByteArray());
|
||||||
Imf::RgbaInputFile file(istr);
|
Imf::RgbaInputFile file(istr);
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get image info
|
||||||
Imath::Box2i dw = file.dataWindow();
|
Imath::Box2i dw = file.dataWindow();
|
||||||
bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
|
qint32 width = dw.max.x - dw.min.x + 1;
|
||||||
|
qint32 height = dw.max.y - dw.min.y + 1;
|
||||||
|
|
||||||
width = dw.max.x - dw.min.x + 1;
|
// limiting the maximum image size on a reasonable size (as done in other plugins)
|
||||||
height = dw.max.y - dw.min.y + 1;
|
if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) {
|
||||||
|
qWarning() << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(EXR_USE_LEGACY_CONVERSIONS)
|
// creating the image
|
||||||
QImage image = imageAlloc(width, height, isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
QImage image = imageAlloc(width, height, imageFormat(file));
|
||||||
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
|
|
||||||
QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
|
|
||||||
#else
|
|
||||||
QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
|
|
||||||
#endif
|
|
||||||
if (image.isNull()) {
|
if (image.isNull()) {
|
||||||
qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height);
|
qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set some useful metadata
|
Imf::Array2D<Imf::Rgba> pixels;
|
||||||
auto &&h = file.header();
|
pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
|
||||||
if (auto comments = h.findTypedAttribute<Imf::StringAttribute>("comments")) {
|
bool isRgba = image.hasAlphaChannel();
|
||||||
image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value()));
|
|
||||||
}
|
|
||||||
if (auto owner = h.findTypedAttribute<Imf::StringAttribute>("owner")) {
|
|
||||||
image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value()));
|
|
||||||
}
|
|
||||||
if (auto capDate = h.findTypedAttribute<Imf::StringAttribute>("capDate")) {
|
|
||||||
float off = 0;
|
|
||||||
if (auto utcOffset = h.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) {
|
|
||||||
off = utcOffset->value();
|
|
||||||
}
|
|
||||||
auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
|
|
||||||
if (dateTime.isValid()) {
|
|
||||||
dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
|
|
||||||
image.setText(QStringLiteral("Date"), dateTime.toString(Qt::ISODate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (auto xDensity = h.findTypedAttribute<Imf::FloatAttribute>("xDensity")) {
|
|
||||||
float par = 1;
|
|
||||||
if (auto pixelAspectRatio = h.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) {
|
|
||||||
par = pixelAspectRatio->value();
|
|
||||||
}
|
|
||||||
image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54));
|
|
||||||
image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54));
|
|
||||||
}
|
|
||||||
|
|
||||||
Imf::Array<Imf::Rgba> pixels;
|
|
||||||
pixels.resizeErase(width);
|
|
||||||
|
|
||||||
// somehow copy pixels into image
|
// somehow copy pixels into image
|
||||||
for (int y = 0; y < height; ++y) {
|
for (int y = 0, n = 0; y < height; y += n) {
|
||||||
auto my = dw.min.y + y;
|
auto my = dw.min.y + y;
|
||||||
if (my <= dw.max.y) { // paranoia check
|
if (my > dw.max.y) { // paranoia check
|
||||||
file.setFrameBuffer(&pixels[0] - dw.min.x - qint64(my) * width, 1, width);
|
break;
|
||||||
file.readPixels(my, my);
|
}
|
||||||
|
|
||||||
|
file.setFrameBuffer(&pixels[0][0] - dw.min.x - qint64(my) * width, 1, width);
|
||||||
|
file.readPixels(my, std::min(my + EXR_LINES_PER_BLOCK - 1, dw.max.y));
|
||||||
|
|
||||||
|
for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
|
||||||
#if defined(EXR_USE_LEGACY_CONVERSIONS)
|
#if defined(EXR_USE_LEGACY_CONVERSIONS)
|
||||||
auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y));
|
Q_UNUSED(isRgba)
|
||||||
|
auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y + n));
|
||||||
for (int x = 0; x < width; ++x) {
|
for (int x = 0; x < width; ++x) {
|
||||||
*(scanLine + x) = RgbaToQrgba(pixels[x]);
|
*(scanLine + x) = RgbaToQrgba(pixels[n][x]);
|
||||||
}
|
}
|
||||||
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
|
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
|
||||||
auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y));
|
auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y + n));
|
||||||
for (int x = 0; x < width; ++x) {
|
for (int x = 0; x < width; ++x) {
|
||||||
auto xcs = x * 4;
|
auto xcs = x * 4;
|
||||||
*(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[x].r), 1.f));
|
*(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[n][x].r), 1.f));
|
||||||
*(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[x].g), 1.f));
|
*(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[n][x].g), 1.f));
|
||||||
*(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[x].b), 1.f));
|
*(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[n][x].b), 1.f));
|
||||||
*(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[x].a), 1.f) : 1.f);
|
*(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[n][x].a), 1.f) : 1.f);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y));
|
auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y + n));
|
||||||
for (int x = 0; x < width; ++x) {
|
for (int x = 0; x < width; ++x) {
|
||||||
*(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[x].r) * 65535.f + 0.5f, 65535.f)),
|
*(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[n][x].r) * 65535.f + 0.5f, 65535.f)),
|
||||||
quint16(qBound(0.f, float(pixels[x].g) * 65535.f + 0.5f, 65535.f)),
|
quint16(qBound(0.f, float(pixels[n][x].g) * 65535.f + 0.5f, 65535.f)),
|
||||||
quint16(qBound(0.f, float(pixels[x].b) * 65535.f + 0.5f, 65535.f)),
|
quint16(qBound(0.f, float(pixels[n][x].b) * 65535.f + 0.5f, 65535.f)),
|
||||||
isRgba ? quint16(qBound(0.f, float(pixels[x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535));
|
isRgba ? quint16(qBound(0.f, float(pixels[n][x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set some useful metadata
|
||||||
|
readMetadata(header, image);
|
||||||
// final color operations
|
// final color operations
|
||||||
#ifndef EXR_USE_LEGACY_CONVERSIONS
|
readColorSpace(header, image);
|
||||||
image.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
|
|
||||||
#ifndef EXR_ALLOW_LINEAR_COLORSPACE
|
|
||||||
image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
|
|
||||||
#endif // !EXR_ALLOW_LINEAR_COLORSPACE
|
|
||||||
#endif // !EXR_USE_LEGACY_CONVERSIONS
|
|
||||||
|
|
||||||
*outImage = image;
|
*outImage = image;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception &exc) {
|
} catch (const std::exception &) {
|
||||||
// qDebug() << exc.what();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief makePreview
|
||||||
|
* Creates a preview of maximum 256 x 256 pixels from the \a image.
|
||||||
|
*/
|
||||||
|
bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels)
|
||||||
|
{
|
||||||
|
auto w = image.width();
|
||||||
|
auto h = image.height();
|
||||||
|
|
||||||
|
QImage preview;
|
||||||
|
if (w > h) {
|
||||||
|
preview = image.scaledToWidth(256).convertToFormat(QImage::Format_ARGB32);
|
||||||
|
} else {
|
||||||
|
preview = image.scaledToHeight(256).convertToFormat(QImage::Format_ARGB32);
|
||||||
|
}
|
||||||
|
if (preview.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
w = preview.width();
|
||||||
|
h = preview.height();
|
||||||
|
pixels.resizeErase(h, w);
|
||||||
|
preview.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||||
|
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
auto scanLine = reinterpret_cast<const QRgb *>(preview.constScanLine(y));
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
auto &&out = pixels[y][x];
|
||||||
|
out.r = qRed(*(scanLine + x));
|
||||||
|
out.g = qGreen(*(scanLine + x));
|
||||||
|
out.b = qBlue(*(scanLine + x));
|
||||||
|
out.a = qAlpha(*(scanLine + x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief setMetadata
|
||||||
|
* Reades the metadata from \a image and set its as attributes in the \a header.
|
||||||
|
*/
|
||||||
|
static void setMetadata(const QImage &image, Imf::Header &header)
|
||||||
|
{
|
||||||
|
auto dateTime = QDateTime::currentDateTime();
|
||||||
|
for (auto &&key : image.textKeys()) {
|
||||||
|
auto text = image.text(key);
|
||||||
|
if (!key.compare(QStringLiteral("Comment"), Qt::CaseInsensitive)) {
|
||||||
|
header.insert("comments", Imf::StringAttribute(text.toStdString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!key.compare(QStringLiteral("Owner"), Qt::CaseInsensitive)) {
|
||||||
|
header.insert("owner", Imf::StringAttribute(text.toStdString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
if (!key.compare(QStringLiteral("Latitude"), Qt::CaseInsensitive) ||
|
||||||
|
!key.compare(QStringLiteral("Longitude"), Qt::CaseInsensitive) ||
|
||||||
|
!key.compare(QStringLiteral("Altitude"), Qt::CaseInsensitive)) {
|
||||||
|
// clang-format on
|
||||||
|
auto ok = false;
|
||||||
|
auto value = QLocale::c().toFloat(text, &ok);
|
||||||
|
if (ok) {
|
||||||
|
header.insert(qPrintable(key.toLower()), Imf::FloatAttribute(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!key.compare(QStringLiteral("CreationDate"), Qt::CaseInsensitive)) {
|
||||||
|
auto dt = QDateTime::fromString(text, Qt::ISODate);
|
||||||
|
if (dt.isValid()) {
|
||||||
|
dateTime = dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef EXR_DISABLE_XMP_ATTRIBUTE // warning: Non-standard attribute!
|
||||||
|
if (!key.compare(QStringLiteral("XML:com.adobe.xmp"), Qt::CaseInsensitive)) {
|
||||||
|
header.insert("xmp", Imf::StringAttribute(text.toStdString()));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (dateTime.isValid()) {
|
||||||
|
header.insert("capDate", Imf::StringAttribute(dateTime.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toStdString()));
|
||||||
|
header.insert("utcOffset", Imf::FloatAttribute(dateTime.offsetFromUtc()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image.dotsPerMeterX() && image.dotsPerMeterY()) {
|
||||||
|
header.insert("xDensity", Imf::FloatAttribute(image.dotsPerMeterX() * 2.54f / 100.f));
|
||||||
|
header.insert("pixelAspectRatio", Imf::FloatAttribute(float(image.dotsPerMeterX()) / float(image.dotsPerMeterY())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default chroma (default constructor ITU-R BT.709-3 -> sRGB)
|
||||||
|
// The image is converted to Linear sRGB so, the chroma is the default EXR value.
|
||||||
|
// If a file doesn’t have a chromaticities attribute, display software should assume that the
|
||||||
|
// file’s primaries and the white point match Rec. ITU-R BT.709-3.
|
||||||
|
// header.insert("chromaticities", Imf::ChromaticitiesAttribute(Imf::Chromaticities()));
|
||||||
|
|
||||||
|
// TODO: EXR 3.2 attributes (see readMetadata())
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EXRHandler::write(const QImage &image)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// create EXR header
|
||||||
|
qint32 width = image.width();
|
||||||
|
qint32 height = image.height();
|
||||||
|
|
||||||
|
// limiting the maximum image size on a reasonable size (as done in other plugins)
|
||||||
|
if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) {
|
||||||
|
qWarning() << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Imf::Header header(width, height);
|
||||||
|
// set compression scheme (forcing PIZ as default)
|
||||||
|
header.compression() = Imf::Compression::PIZ_COMPRESSION;
|
||||||
|
if (m_compressionRatio >= qint32(Imf::Compression::NO_COMPRESSION) && m_compressionRatio < qint32(Imf::Compression::NUM_COMPRESSION_METHODS)) {
|
||||||
|
header.compression() = Imf::Compression(m_compressionRatio);
|
||||||
|
}
|
||||||
|
// set the DCT quality (used by DCT compressions only)
|
||||||
|
if (m_quality > -1 && m_quality <= 100) {
|
||||||
|
header.dwaCompressionLevel() = float(m_quality);
|
||||||
|
}
|
||||||
|
// make ZIP compression fast (used by ZIP compressions)
|
||||||
|
header.zipCompressionLevel() = 1;
|
||||||
|
|
||||||
|
// set preview (don't set it for small images)
|
||||||
|
if (width > 1024 || height > 1024) {
|
||||||
|
Imf::Array2D<Imf::PreviewRgba> previewPixels;
|
||||||
|
if (makePreview(image, previewPixels)) {
|
||||||
|
header.setPreviewImage(Imf::PreviewImage(previewPixels.width(), previewPixels.height(), &previewPixels[0][0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set metadata (EXR attributes)
|
||||||
|
setMetadata(image, header);
|
||||||
|
|
||||||
|
// write the EXR
|
||||||
|
K_OStream ostr(device(), QByteArray());
|
||||||
|
Imf::RgbaOutputFile file(ostr, header, image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB);
|
||||||
|
Imf::Array2D<Imf::Rgba> pixels;
|
||||||
|
pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
|
||||||
|
|
||||||
|
// convert the image and write into the stream
|
||||||
|
#if defined(EXR_USE_QT6_FLOAT_IMAGE)
|
||||||
|
auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4;
|
||||||
|
#else
|
||||||
|
auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA64 : QImage::Format_RGBX64;
|
||||||
|
#endif
|
||||||
|
ScanLineConverter slc(convFormat);
|
||||||
|
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
|
||||||
|
slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear));
|
||||||
|
for (int y = 0, n = 0; y < height; y += n) {
|
||||||
|
for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
|
||||||
|
#if defined(EXR_USE_QT6_FLOAT_IMAGE)
|
||||||
|
auto scanLine = reinterpret_cast<const qfloat16 *>(slc.convertedScanLine(image, y + n));
|
||||||
|
if (scanLine == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
auto xcs = x * 4;
|
||||||
|
pixels[n][x].r = float(*(scanLine + xcs));
|
||||||
|
pixels[n][x].g = float(*(scanLine + xcs + 1));
|
||||||
|
pixels[n][x].b = float(*(scanLine + xcs + 2));
|
||||||
|
pixels[n][x].a = float(*(scanLine + xcs + 3));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
auto scanLine = reinterpret_cast<const QRgba64 *>(slc.convertedScanLine(image, y + n));
|
||||||
|
if (scanLine == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
pixels[n][x].r = float((scanLine + x)->red() / 65535.f);
|
||||||
|
pixels[n][x].g = float((scanLine + x)->green() / 65535.f);
|
||||||
|
pixels[n][x].b = float((scanLine + x)->blue() / 65535.f);
|
||||||
|
pixels[n][x].a = float((scanLine + x)->alpha() / 65535.f);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
file.setFrameBuffer(&pixels[0][0] - qint64(y) * width, 1, width);
|
||||||
|
file.writePixels(n);
|
||||||
|
}
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EXRHandler::setOption(ImageOption option, const QVariant &value)
|
||||||
|
{
|
||||||
|
if (option == QImageIOHandler::CompressionRatio) {
|
||||||
|
auto ok = false;
|
||||||
|
auto cr = value.toInt(&ok);
|
||||||
|
if (ok) {
|
||||||
|
m_compressionRatio = cr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::Quality) {
|
||||||
|
auto ok = false;
|
||||||
|
auto q = value.toInt(&ok);
|
||||||
|
if (ok) {
|
||||||
|
m_quality = q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EXRHandler::supportsOption(ImageOption option) const
|
||||||
|
{
|
||||||
|
if (option == QImageIOHandler::Size) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::ImageFormat) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::CompressionRatio) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::Quality) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant EXRHandler::option(ImageOption option) const
|
||||||
|
{
|
||||||
|
QVariant v;
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::Size) {
|
||||||
|
if (auto d = device()) {
|
||||||
|
// transactions works on both random and sequential devices
|
||||||
|
d->startTransaction();
|
||||||
|
try {
|
||||||
|
K_IStream istr(d, QByteArray());
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 &) {
|
||||||
|
// broken file or unsupported version
|
||||||
|
}
|
||||||
|
d->rollbackTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::ImageFormat) {
|
||||||
|
if (auto d = device()) {
|
||||||
|
// transactions works on both random and sequential devices
|
||||||
|
d->startTransaction();
|
||||||
|
try {
|
||||||
|
K_IStream istr(d, QByteArray());
|
||||||
|
Imf::RgbaInputFile file(istr);
|
||||||
|
v = QVariant::fromValue(imageFormat(file));
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
// broken file or unsupported version
|
||||||
|
}
|
||||||
|
d->rollbackTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::CompressionRatio) {
|
||||||
|
v = QVariant(m_compressionRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::Quality) {
|
||||||
|
v = QVariant(m_quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EXRHandler::jumpToNextImage()
|
||||||
|
{
|
||||||
|
return jumpToImage(m_imageNumber + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EXRHandler::jumpToImage(int imageNumber)
|
||||||
|
{
|
||||||
|
if (imageNumber >= imageCount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_imageNumber = imageNumber;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EXRHandler::imageCount() const
|
||||||
|
{
|
||||||
|
// NOTE: image count is cached for performance reason
|
||||||
|
auto &&count = m_imageCount;
|
||||||
|
if (count > 0) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
count = QImageIOHandler::imageCount();
|
||||||
|
|
||||||
|
auto d = device();
|
||||||
|
d->startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
K_IStream istr(d, QByteArray());
|
||||||
|
Imf::RgbaInputFile file(istr);
|
||||||
|
auto views = viewList(file.header());
|
||||||
|
if (!views.isEmpty()) {
|
||||||
|
count = views.size();
|
||||||
|
}
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
d->rollbackTransaction();
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EXRHandler::currentImageNumber() const
|
||||||
|
{
|
||||||
|
return m_imageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
bool EXRHandler::canRead(QIODevice *device)
|
bool EXRHandler::canRead(QIODevice *device)
|
||||||
{
|
{
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@ -273,7 +828,7 @@ bool EXRHandler::canRead(QIODevice *device)
|
|||||||
QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||||
{
|
{
|
||||||
if (format == "exr") {
|
if (format == "exr") {
|
||||||
return Capabilities(CanRead);
|
return Capabilities(CanRead | CanWrite);
|
||||||
}
|
}
|
||||||
if (!format.isEmpty()) {
|
if (!format.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
@ -286,6 +841,9 @@ QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QB
|
|||||||
if (device->isReadable() && EXRHandler::canRead(device)) {
|
if (device->isReadable() && EXRHandler::canRead(device)) {
|
||||||
cap |= CanRead;
|
cap |= CanRead;
|
||||||
}
|
}
|
||||||
|
if (device->isWritable()) {
|
||||||
|
cap |= CanWrite;
|
||||||
|
}
|
||||||
return cap;
|
return cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
QImageIO Routines to read (and perhaps in the future, write) images
|
The high dynamic range EXR format support for QImage.
|
||||||
in the high definition EXR format.
|
|
||||||
|
|
||||||
SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
|
SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
|
||||||
|
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||||
|
|
||||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
@ -12,6 +12,44 @@
|
|||||||
|
|
||||||
#include <QImageIOPlugin>
|
#include <QImageIOPlugin>
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The EXRHandler class
|
||||||
|
* The handler uses the OpenEXR reference implementation of the EXR file format.
|
||||||
|
*
|
||||||
|
* The purpose of EXR format is to accurately and efficiently represent high-dynamic-range scene-linear
|
||||||
|
* image data and associated metadata.
|
||||||
|
*
|
||||||
|
* Both reading and writing of EXR files is supported. When saving, the image is converted to 16-bit
|
||||||
|
* and sRGB Linear color space (if not already so). If no color space is set in the image, sRGB is assumed.
|
||||||
|
* When the handler is compiled with the default compile options, the loaded image is a 16-bit image
|
||||||
|
* with linear color space.
|
||||||
|
* Multiview images are also supported (read only) via imageCount(), jumpToImage(), etc....
|
||||||
|
*
|
||||||
|
* The following QImageIOHandler options are supported:
|
||||||
|
* - ImageFormat: The image's data format returned by the handler.
|
||||||
|
* - Size: The size of the image.
|
||||||
|
* - CompressionRatio: The compression ratio of the image data (see OpenEXR compression schemes).
|
||||||
|
* - Quality: The quality level of the image (see OpenEXR compression level of lossy codecs).
|
||||||
|
*
|
||||||
|
* The following metadata are set/get via QImage::setText()/QImage::text() in both read/write (if any):
|
||||||
|
* - Latitude, Longitude, Altitude: Geographic coordinates (Float converted to string).
|
||||||
|
* - CreationDate: Date the image was captured/created (QDateTime converted to string using Qt::ISODate).
|
||||||
|
* - Comment: Additional image information in human-readable form, for example a verbal description of the image (QString).
|
||||||
|
* - Owner: Name of the owner of the image (QString).
|
||||||
|
*
|
||||||
|
* In addition, information about image resolution is preserved and the preview is written for images larger
|
||||||
|
* than 1024px.
|
||||||
|
*
|
||||||
|
* The following compile options are supported (defines):
|
||||||
|
* - EXR_MAX_IMAGE_WIDTH: Maximum image width supported (read/write, default: 300000 px).
|
||||||
|
* - EXR_MAX_IMAGE_HEIGHT: Maximum image height supported (read/write, default: 300000 px).
|
||||||
|
* - EXR_LINES_PER_BLOCK: The number of scanlines buffered on both read and write operations.\n
|
||||||
|
* The higher the value, the greater the parallelization but the RAM consumption increases (default: 128)
|
||||||
|
* - EXR_USE_LEGACY_CONVERSIONS: The result image is an 8-bit RGB(A) converted without icc profiles (read, default: undefined).
|
||||||
|
* - EXR_CONVERT_TO_SRGB: The resulting image is convertef in the sRGB color space (read, default: undefined).
|
||||||
|
* - EXR_DISABLE_XMP_ATTRIBUTE: Disable the stores of XMP values in a non-standard attribute named "xmp".\n
|
||||||
|
* The QImage metadata used is "XML:com.adobe.xmp" (write, default: undefined).
|
||||||
|
*/
|
||||||
class EXRHandler : public QImageIOHandler
|
class EXRHandler : public QImageIOHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -19,8 +57,65 @@ public:
|
|||||||
|
|
||||||
bool canRead() const override;
|
bool canRead() const override;
|
||||||
bool read(QImage *outImage) override;
|
bool read(QImage *outImage) override;
|
||||||
|
bool write(const QImage &image) override;
|
||||||
|
|
||||||
|
void setOption(ImageOption option, const QVariant &value) override;
|
||||||
|
bool supportsOption(QImageIOHandler::ImageOption option) const override;
|
||||||
|
QVariant option(QImageIOHandler::ImageOption option) const override;
|
||||||
|
|
||||||
|
bool jumpToNextImage() override;
|
||||||
|
bool jumpToImage(int imageNumber) override;
|
||||||
|
int imageCount() const override;
|
||||||
|
int currentImageNumber() const override;
|
||||||
|
|
||||||
static bool canRead(QIODevice *device);
|
static bool canRead(QIODevice *device);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/*!
|
||||||
|
* \brief m_compressionRatio
|
||||||
|
* Value set by QImageWriter::setCompression().
|
||||||
|
*
|
||||||
|
* The compression scheme is the same as defined by OpenEXR library:
|
||||||
|
* - 0: no compression
|
||||||
|
* - 1: run length encoding
|
||||||
|
* - 2: zlib compression, one scan line at a time
|
||||||
|
* - 3: zlib compression, in blocks of 16 scan lines
|
||||||
|
* - 4: piz-based wavelet compression (default)
|
||||||
|
* - 5: lossy 24-bit float compression
|
||||||
|
* - 6: lossy 4-by-4 pixel block compression, fixed compression rate
|
||||||
|
* - 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.
|
||||||
|
*/
|
||||||
|
qint32 m_compressionRatio;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief m_quality
|
||||||
|
* Value set by QImageWriter::setQuality().
|
||||||
|
*
|
||||||
|
* The quality is used on DCT compression schemes only with a
|
||||||
|
* supposed value between 0 and 100 (default: 45).
|
||||||
|
*/
|
||||||
|
qint32 m_quality;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief m_imageNumber
|
||||||
|
* Value set by QImageReader::jumpToImage() or QImageReader::jumpToNextImage().
|
||||||
|
* The number of view selected in a multiview image.
|
||||||
|
*/
|
||||||
|
qint32 m_imageNumber;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief m_imageCount
|
||||||
|
* The total number of views (cache value)
|
||||||
|
*/
|
||||||
|
mutable qint32 m_imageCount;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief m_startPos
|
||||||
|
* The initial device position to allow multi image load (cache value).
|
||||||
|
*/
|
||||||
|
qint64 m_startPos;
|
||||||
};
|
};
|
||||||
|
|
||||||
class EXRPlugin : public QImageIOPlugin
|
class EXRPlugin : public QImageIOPlugin
|
||||||
|
@ -15,13 +15,15 @@ ScanLineConverter::ScanLineConverter(const QImage::Format &targetFormat)
|
|||||||
ScanLineConverter::ScanLineConverter(const ScanLineConverter &other)
|
ScanLineConverter::ScanLineConverter(const ScanLineConverter &other)
|
||||||
: _targetFormat(other._targetFormat)
|
: _targetFormat(other._targetFormat)
|
||||||
, _colorSpace(other._colorSpace)
|
, _colorSpace(other._colorSpace)
|
||||||
|
, _defaultColorSpace(other._defaultColorSpace)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanLineConverter &ScanLineConverter::operator=(const ScanLineConverter &other)
|
ScanLineConverter &ScanLineConverter::operator=(const ScanLineConverter &other)
|
||||||
{
|
{
|
||||||
this->_targetFormat = other._targetFormat;
|
_targetFormat = other._targetFormat;
|
||||||
this->_colorSpace = other._colorSpace;
|
_colorSpace = other._colorSpace;
|
||||||
|
_defaultColorSpace = other._defaultColorSpace;
|
||||||
return (*this);
|
return (*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +42,19 @@ QColorSpace ScanLineConverter::targetColorSpace() const
|
|||||||
return _colorSpace;
|
return _colorSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScanLineConverter::setDefaultSourceColorSpace(const QColorSpace &colorSpace)
|
||||||
|
{
|
||||||
|
_defaultColorSpace = colorSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
QColorSpace ScanLineConverter::defaultSourceColorSpace() const
|
||||||
|
{
|
||||||
|
return _defaultColorSpace;
|
||||||
|
}
|
||||||
|
|
||||||
const uchar *ScanLineConverter::convertedScanLine(const QImage &image, qint32 y)
|
const uchar *ScanLineConverter::convertedScanLine(const QImage &image, qint32 y)
|
||||||
{
|
{
|
||||||
auto colorSpaceConversion = isColorSpaceConversionNeeded(image, _colorSpace);
|
auto colorSpaceConversion = isColorSpaceConversionNeeded(image);
|
||||||
if (image.format() == _targetFormat && !colorSpaceConversion) {
|
if (image.format() == _targetFormat && !colorSpaceConversion) {
|
||||||
return image.constScanLine(y);
|
return image.constScanLine(y);
|
||||||
}
|
}
|
||||||
@ -54,7 +66,11 @@ const uchar *ScanLineConverter::convertedScanLine(const QImage &image, qint32 y)
|
|||||||
}
|
}
|
||||||
std::memcpy(_tmpBuffer.bits(), image.constScanLine(y), std::min(_tmpBuffer.bytesPerLine(), image.bytesPerLine()));
|
std::memcpy(_tmpBuffer.bits(), image.constScanLine(y), std::min(_tmpBuffer.bytesPerLine(), image.bytesPerLine()));
|
||||||
if (colorSpaceConversion) {
|
if (colorSpaceConversion) {
|
||||||
_tmpBuffer.setColorSpace(image.colorSpace());
|
auto cs = image.colorSpace();
|
||||||
|
if (!cs.isValid()) {
|
||||||
|
cs = _defaultColorSpace;
|
||||||
|
}
|
||||||
|
_tmpBuffer.setColorSpace(cs);
|
||||||
_tmpBuffer.convertToColorSpace(_colorSpace);
|
_tmpBuffer.convertToColorSpace(_colorSpace);
|
||||||
}
|
}
|
||||||
_convBuffer = _tmpBuffer.convertToFormat(_targetFormat);
|
_convBuffer = _tmpBuffer.convertToFormat(_targetFormat);
|
||||||
@ -72,12 +88,15 @@ qsizetype ScanLineConverter::bytesPerLine() const
|
|||||||
return _convBuffer.bytesPerLine();
|
return _convBuffer.bytesPerLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScanLineConverter::isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace) const
|
bool ScanLineConverter::isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace, const QColorSpace &defaultColorSpace)
|
||||||
{
|
{
|
||||||
if (image.depth() < 24) { // RGB 8 bit or grater only
|
if (image.depth() < 24) { // RGB 8 bit or grater only
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto sourceColorSpace = image.colorSpace();
|
auto sourceColorSpace = image.colorSpace();
|
||||||
|
if (!sourceColorSpace.isValid()) {
|
||||||
|
sourceColorSpace = defaultColorSpace;
|
||||||
|
}
|
||||||
if (!sourceColorSpace.isValid() || !targetColorSpace.isValid()) {
|
if (!sourceColorSpace.isValid() || !targetColorSpace.isValid()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,14 @@ public:
|
|||||||
void setTargetColorSpace(const QColorSpace &colorSpace);
|
void setTargetColorSpace(const QColorSpace &colorSpace);
|
||||||
QColorSpace targetColorSpace() const;
|
QColorSpace targetColorSpace() const;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief setDefaultSourceColorSpace
|
||||||
|
* Set the source color space to use when the image does not have a color space.
|
||||||
|
* \param colorSpace
|
||||||
|
*/
|
||||||
|
void setDefaultSourceColorSpace(const QColorSpace &colorSpace);
|
||||||
|
QColorSpace defaultSourceColorSpace() const;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief convertedScanLine
|
* \brief convertedScanLine
|
||||||
* Convert the scanline \a y.
|
* Convert the scanline \a y.
|
||||||
@ -62,14 +70,24 @@ public:
|
|||||||
* \note Only 24 bit or grater images.
|
* \note Only 24 bit or grater images.
|
||||||
* \param image The source image.
|
* \param image The source image.
|
||||||
* \param targetColorSpace The target color space.
|
* \param targetColorSpace The target color space.
|
||||||
|
* \param defaultColorSpace The default color space to use it image does not contains one.
|
||||||
* \return True if the conversion should be done otherwise false.
|
* \return True if the conversion should be done otherwise false.
|
||||||
*/
|
*/
|
||||||
bool isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace) const;
|
static bool isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace, const QColorSpace &defaultColorSpace = QColorSpace());
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief isColorSpaceConversionNeeded
|
||||||
|
*/
|
||||||
|
inline bool isColorSpaceConversionNeeded(const QImage &image) const
|
||||||
|
{
|
||||||
|
return isColorSpaceConversionNeeded(image, _colorSpace, _defaultColorSpace);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// data
|
// data
|
||||||
QImage::Format _targetFormat;
|
QImage::Format _targetFormat;
|
||||||
QColorSpace _colorSpace;
|
QColorSpace _colorSpace;
|
||||||
|
QColorSpace _defaultColorSpace;
|
||||||
|
|
||||||
// internal buffers
|
// internal buffers
|
||||||
QImage _tmpBuffer;
|
QImage _tmpBuffer;
|
||||||
|
Loading…
Reference in New Issue
Block a user