kimageformats/src/imageformats/jxr.cpp
2025-05-06 12:39:41 +02:00

1264 lines
46 KiB
C++

/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
/*
* Info about JXR:
* - https://learn.microsoft.com/en-us/windows/win32/wic/jpeg-xr-codec
*
* Sample images:
* - http://fileformats.archiveteam.org/wiki/JPEG_XR
* - https://github.com/bvibber/hdrfix/tree/main/samples
*/
#include "jxr_p.h"
#include "microexif_p.h"
#include "util_p.h"
#include <QColorSpace>
#include <QCoreApplication>
#include <QDataStream>
#include <QFile>
#include <QFloat16>
#include <QHash>
#include <QImage>
#include <QImageReader>
#include <QLoggingCategory>
#include <QSet>
#include <QSharedData>
#include <QTemporaryDir>
#include <JXRGlue.h>
#include <cstring>
Q_DECLARE_LOGGING_CATEGORY(LOG_JXRPLUGIN)
Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
/*!
* 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
/*!
* Remove the neeeds of additional memory by disabling the conversion between
* different color depths (e.g. RGBA64bpp to RGBA32bpp).
*
* NOTE: Leaving deptch conversion enabled (default) ensures maximum read compatibility.
*/
// #define JXR_DISABLE_DEPTH_CONVERSION // default commented
/*!
* Windows displays and opens JXR files correctly out of the box. Unfortunately it doesn't
* seem to open (P)RGBA @32bpp files as it only wants (P)BGRA32bpp files (a format not supported by Qt).
* Only for this format an hack is activated to guarantee total compatibility of the plugin with Windows.
*/
// #define JXR_DISABLE_BGRA_HACK // default commented
/*!
* The following functions are present in the Debian headers but not in the SUSE ones even if the source version is 1.0.1 on both.
*
* - ERR PKImageDecode_GetXMPMetadata_WMP(PKImageDecode *pID, U8 *pbXMPMetadata, U32 *pcbXMPMetadata);
* - ERR PKImageDecode_GetEXIFMetadata_WMP(PKImageDecode *pID, U8 *pbEXIFMetadata, U32 *pcbEXIFMetadata);
* - ERR PKImageDecode_GetGPSInfoMetadata_WMP(PKImageDecode *pID, U8 *pbGPSInfoMetadata, U32 *pcbGPSInfoMetadata);
* - ERR PKImageDecode_GetIPTCNAAMetadata_WMP(PKImageDecode *pID, U8 *pbIPTCNAAMetadata, U32 *pcbIPTCNAAMetadata);
* - ERR PKImageDecode_GetPhotoshopMetadata_WMP(PKImageDecode *pID, U8 *pbPhotoshopMetadata, U32 *pcbPhotoshopMetadata);
*
* As a result, their use is disabled by default. It is possible to activate their use by defining the
* JXR_ENABLE_ADVANCED_METADATA preprocessor directive
*/
// #define JXR_ENABLE_ADVANCED_METADATA
#ifndef JXR_MAX_METADATA_SIZE
/*!
* XMP and EXIF maximum size.
*/
#define JXR_MAX_METADATA_SIZE (4 * 1024 * 1024)
#endif
class JXRHandlerPrivate : public QSharedData
{
private:
QSharedPointer<QTemporaryDir> m_tempDir;
QSharedPointer<QFile> m_jxrFile;
MicroExif m_exif;
qint32 m_quality;
QImageIOHandler::Transformations m_transformations;
mutable QHash<QString, QString> m_txtMeta;
public:
PKFactory *pFactory = nullptr;
PKCodecFactory *pCodecFactory = nullptr;
PKImageDecode *pDecoder = nullptr;
PKImageEncode *pEncoder = nullptr;
JXRHandlerPrivate()
: m_quality(-1)
, 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!";
}
}
JXRHandlerPrivate(const JXRHandlerPrivate &other) = default;
~JXRHandlerPrivate()
{
if (pCodecFactory) {
PKCreateCodecFactory_Release(&pCodecFactory);
}
if (pFactory) {
PKCreateFactory_Release(&pFactory);
}
if (pDecoder) {
PKImageDecode_Release(&pDecoder);
}
if (pEncoder) {
PKImageEncode_Release(&pEncoder);
}
}
QString fileName() const
{
return m_jxrFile->fileName();
}
/*!
* \brief setQuality
* Set the image quality (write only)
* \param q
*/
void setQuality(qint32 q)
{
m_quality = q;
}
qint32 quality() const
{
return m_quality;
}
/*!
* \brief setTransformation
* Set the image transformation (read/write)
* \param t
*/
void setTransformation(const QImageIOHandler::Transformations& t)
{
m_transformations = t;
}
QImageIOHandler::Transformations transformation() const
{
return m_transformations;
}
static QImageIOHandler::Transformations orientationToTransformation(const ORIENTATION& o)
{
auto v = QImageIOHandler::TransformationNone;
switch (o) {
case O_FLIPV:
v = QImageIOHandler::TransformationFlip;
break;
case O_FLIPH:
v = QImageIOHandler::TransformationMirror;
break;
case O_FLIPVH:
v = QImageIOHandler::TransformationRotate180;
break;
case O_RCW:
v = QImageIOHandler::TransformationRotate90;
break;
case O_RCW_FLIPH:
v = QImageIOHandler::TransformationFlipAndRotate90;
break;
case O_RCW_FLIPV:
v = QImageIOHandler::TransformationMirrorAndRotate90;
break;
case O_RCW_FLIPVH:
v = QImageIOHandler::TransformationRotate270;
break;
default:
v = QImageIOHandler::TransformationNone;
break;
}
return v;
}
static ORIENTATION transformationToOrientation(const QImageIOHandler::Transformations& t)
{
auto v = O_NONE;
switch (t) {
case QImageIOHandler::TransformationFlip:
v = O_FLIPV;
break;
case QImageIOHandler::TransformationMirror:
v = O_FLIPH;
break;
case QImageIOHandler::TransformationRotate180:
v = O_FLIPVH;
break;
case QImageIOHandler::TransformationRotate90:
v = O_RCW;
break;
case QImageIOHandler::TransformationFlipAndRotate90:
v = O_RCW_FLIPH;
break;
case QImageIOHandler::TransformationMirrorAndRotate90:
v = O_RCW_FLIPV;
break;
case QImageIOHandler::TransformationRotate270:
v = O_RCW_FLIPVH;
break;
default:
v = O_NONE;
break;
}
return v;
}
/* *** READ *** */
/*!
* \brief initForReading
* Initialize the device for reading.
* \param device The source device.
* \return True on success, otherwise false.
*/
bool initForReading(QIODevice *device)
{
if (!readDevice(device)) {
return false;
}
if (!initDecoder()) {
return false;
}
return true;
}
/*!
* \brief initForReadingAndRollBack
* Initialize the device for reading and rollback the device to start position.
* \param device The source device.
* \return True on success, otherwise false.
*/
bool initForReadingAndRollBack(QIODevice *device)
{
if (device) {
device->startTransaction();
}
auto ok = initForReading(device);
if (device) {
device->rollbackTransaction();
}
return ok;
}
/*!
* \brief jxrFormat
* \return The JXR format.
*/
PKPixelFormatGUID jxrFormat() const
{
PKPixelFormatGUID pixelFormatGUID = GUID_PKPixelFormatUndefined;
if (pDecoder) {
pDecoder->GetPixelFormat(pDecoder, &pixelFormatGUID);
}
return pixelFormatGUID;
}
/*!
* \brief imageFormat
* Calculate the image format from the JXR format. In conversionFormat it returns the possible conversion format of the JXR to match the returned Qt format.
* \return The QImage format. If invalid, the image cannot be read.
*/
QImage::Format imageFormat(PKPixelFormatGUID *conversionFormat = nullptr) const
{
PKPixelFormatGUID tmp;
if (conversionFormat == nullptr) {
conversionFormat = &tmp;
}
*conversionFormat = GUID_PKPixelFormatUndefined;
auto jxrfmt = jxrFormat();
auto qtFormat = exactFormat(jxrfmt);
if (qtFormat != QImage::Format_Invalid) {
return qtFormat;
}
// *** CONVERSION WITH THE SAME DEPTH ***
// IMPORTANT: For supported conversions see JXRGluePFC.c
// 32-bit
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGR)) {
*conversionFormat = GUID_PKPixelFormat24bppRGB;
return QImage::Format_RGB888;
};
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGRA)) {
*conversionFormat = GUID_PKPixelFormat32bppRGBA;
return QImage::Format_RGBA8888;
};
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppPBGRA)) {
*conversionFormat = GUID_PKPixelFormat32bppPRGBA;
return QImage::Format_RGBA8888_Premultiplied;
};
#ifndef JXR_DENY_FLOAT_IMAGE
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFixedPoint)) {
*conversionFormat = GUID_PKPixelFormat128bppRGBAFloat;
return QImage::Format_RGBA32FPx4;
};
#endif // !JXR_DENY_FLOAT_IMAGE
// *** CONVERSION TO A LOWER DEPTH ***
// IMPORTANT: For supported conversions see JXRGluePFC.c
#ifndef JXR_DISABLE_DEPTH_CONVERSION
#ifndef JXR_DENY_FLOAT_IMAGE
// RGB FLOAT
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFloat)) {
*conversionFormat = GUID_PKPixelFormat64bppRGBHalf;
return QImage::Format_RGBX16FPx4;
};
#endif // !JXR_DENY_FLOAT_IMAGE
// RGBA
// clang-format off
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBAHalf) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBAFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFloat)) {
*conversionFormat = GUID_PKPixelFormat32bppRGBA;
return QImage::Format_RGBA8888;
};
// clang-format on
// RGB
// clang-format off
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBFloat) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFloat) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGBHalf) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBHalf) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGBFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppRGB101010) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGB) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppRGBE) ) {
*conversionFormat = GUID_PKPixelFormat24bppRGB;
return QImage::Format_RGB888;
};
// clang-format on
// Gray
// clang-format off
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppGrayFloat) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat16bppGrayFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppGrayFixedPoint) ||
IsEqualGUID(jxrfmt, GUID_PKPixelFormat16bppGrayHalf)) {
*conversionFormat = GUID_PKPixelFormat8bppGray;
return QImage::Format_Grayscale8;
};
// clang-format on
#endif // !JXR_DISABLE_DEPTH_CONVERSION
return QImage::Format_Invalid;
}
/*!
* \brief imageSize
* \return The image size in pixels.
*/
QSize imageSize() const
{
if (pDecoder) {
qint32 w, h;
pDecoder->GetSize(pDecoder, &w, &h);
return QSize(w, h);
}
return {};
}
/*!
* \brief colorSpace
* \return The ICC profile if exists.
*/
QColorSpace colorSpace() const
{
QColorSpace cs;
if (pDecoder == nullptr) {
return cs;
}
quint32 size;
if (!pDecoder->GetColorContext(pDecoder, nullptr, &size) && size) {
QByteArray ba(size, 0);
if (!pDecoder->GetColorContext(pDecoder, reinterpret_cast<quint8 *>(ba.data()), &size)) {
cs = QColorSpace::fromIccProfile(ba);
}
}
return cs;
}
/*!
* \brief xmpData
* \return The XMP data if exists.
*/
QString xmpData() const
{
QString xmp;
if (pDecoder == nullptr) {
return xmp;
}
#ifdef JXR_ENABLE_ADVANCED_METADATA
quint32 size;
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)) {
xmp = QString::fromUtf8(ba);
}
}
#endif
return xmp;
}
/*!
* \brief exifData
* \return The EXIF data.
*/
MicroExif exifData() const
{
return m_exif;
}
/*!
* \brief setMetadata
* Set the metadata into \a image
* \param image Image on which to write metadata
*/
void setMetadata(QImage& image)
{
auto xmp = xmpData();
if (!xmp.isEmpty()) {
image.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp);
}
auto descr = description();
if (!descr.isEmpty()) {
image.setText(QStringLiteral(META_KEY_DESCRIPTION), descr);
}
auto softw = software();
if (!softw.isEmpty()) {
image.setText(QStringLiteral(META_KEY_SOFTWARE), softw);
}
auto make = cameraMake();
if (!make.isEmpty()) {
image.setText(QStringLiteral(META_KEY_MANUFACTURER), make);
}
auto model = cameraModel();
if (!model.isEmpty()) {
image.setText(QStringLiteral(META_KEY_MODEL), model);
}
auto author = artist();
if (!author.isEmpty()) {
image.setText(QStringLiteral(META_KEY_AUTHOR), author);
}
auto copy = copyright();
if (!copy.isEmpty()) {
image.setText(QStringLiteral(META_KEY_COPYRIGHT), copy);
}
auto capt = caption();
if (!capt.isEmpty()) {
image.setText(QStringLiteral(META_KEY_TITLE), capt);
}
auto host = hostComputer();
if (!host.isEmpty()) {
image.setText(QStringLiteral(META_KEY_HOSTCOMPUTER), capt);
}
auto docn = documentName();
if (!docn.isEmpty()) {
image.setText(QStringLiteral(META_KEY_DOCUMENTNAME), docn);
}
auto exif = exifData();
if (!exif.isEmpty()) {
exif.updateImageMetadata(image);
}
}
#define META_TEXT(name, key) \
QString name() const \
{ \
readTextMeta(); \
return m_txtMeta.value(QStringLiteral(key)); \
}
META_TEXT(description, META_KEY_DESCRIPTION)
META_TEXT(cameraMake, META_KEY_MANUFACTURER)
META_TEXT(cameraModel, META_KEY_MODEL)
META_TEXT(software, META_KEY_SOFTWARE)
META_TEXT(artist, META_KEY_AUTHOR)
META_TEXT(copyright, META_KEY_COPYRIGHT)
META_TEXT(caption, META_KEY_TITLE)
META_TEXT(documentName, META_KEY_DOCUMENTNAME)
META_TEXT(hostComputer, META_KEY_HOSTCOMPUTER)
#undef META_TEXT
/* *** WRITE *** */
/*!
* \brief initForWriting
* Initialize the stream for writing.
* \return True on success, otherwise false.
*/
bool initForWriting()
{
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
QSharedPointer<QFile> file(new QFile(fileName));
m_jxrFile = file;
return initEncoder();
}
/*!
* \brief finalizeWriting
* \param device
* Finalize the writing operation. Must be called as last peration.
* \return True on success, otherwise false.
*/
bool finalizeWriting(QIODevice *device)
{
if (device == nullptr || pEncoder == nullptr) {
return false;
}
if (auto err = PKImageEncode_Release(&pEncoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while releasing the encoder:" << err;
return false;
}
if (!deviceCopy(device, m_jxrFile.data())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while writing in the target device";
return false;
}
return true;
}
/*!
* \brief imageToSave
* If necessary it converts the image to be saved into the appropriate format otherwise it does nothing.
* \param source The image to save.
* \return The image to use for save operation.
*/
QImage imageToSave(const QImage &source) const
{
// IMPORTANT: these values must be in exactMatchingFormat()
// clang-format off
auto valid = QSet<QImage::Format>()
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
<< QImage::Format_CMYK8888
#endif
#ifndef JXR_DENY_FLOAT_IMAGE
<< QImage::Format_RGBA16FPx4
<< QImage::Format_RGBX16FPx4
<< QImage::Format_RGBA32FPx4
<< QImage::Format_RGBA32FPx4_Premultiplied
<< QImage::Format_RGBX32FPx4
#endif // JXR_DENY_FLOAT_IMAGE
<< QImage::Format_RGBA64
<< QImage::Format_RGBA64_Premultiplied
<< QImage::Format_RGBA8888
<< QImage::Format_RGBA8888_Premultiplied
<< QImage::Format_RGBX8888
<< QImage::Format_BGR888
<< QImage::Format_RGB888
<< QImage::Format_RGB555
<< QImage::Format_RGB16
<< QImage::Format_Grayscale16
<< QImage::Format_Grayscale8
<< QImage::Format_Mono;
// clang-format on
// To avoid complex code, I will save only integer formats.
auto qi = source;
if (qi.format() == QImage::Format_MonoLSB) {
qi = qi.convertToFormat(QImage::Format_Mono);
}
if (qi.format() == QImage::Format_Indexed8) {
if (qi.allGray())
qi = qi.convertToFormat(QImage::Format_Grayscale8);
else
qi = qi.convertToFormat(QImage::Format_RGBA8888);
}
#ifndef JXR_DENY_FLOAT_IMAGE
if (qi.format() == QImage::Format_RGBA16FPx4_Premultiplied) {
qi = qi.convertToFormat(QImage::Format_RGBA16FPx4);
}
#endif // JXR_DENY_FLOAT_IMAGE
// generic
if (!valid.contains(qi.format())) {
auto alpha = qi.hasAlphaChannel();
auto depth = qi.depth();
if (depth >= 12 && depth <= 24 && !alpha) {
qi = qi.convertToFormat(QImage::Format_RGB888);
} else if (depth >= 48) {
// JXR don't have RGBX64 format so I have two possibilities:
// - convert to 32 bpp (convertToFormat(alpha ? QImage::Format_RGBA64 : QImage::Format_RGB888))
// - convert to 64 bpp with fake alpha (preferred)
qi = qi.convertToFormat(QImage::Format_RGBA64);
} else {
qi = qi.convertToFormat(alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
}
#ifndef JXR_DENY_FLOAT_IMAGE
// clang-format off
} else if (qi.format() == QImage::Format_RGBA16FPx4 ||
qi.format() == QImage::Format_RGBX16FPx4 ||
qi.format() == QImage::Format_RGBA32FPx4 ||
qi.format() == QImage::Format_RGBA32FPx4_Premultiplied ||
qi.format() == QImage::Format_RGBX32FPx4) {
// clang-format on
auto cs = qi.colorSpace();
if (cs.isValid() && cs.transferFunction() != QColorSpace::TransferFunction::Linear) {
qi = qi.convertedToColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
}
#endif // JXR_DENY_FLOAT_IMAGE
return qi;
}
/*!
* \brief initCodecParameters
* Initialize the JXR codec parameters.
* \param wmiSCP
* \param image The image to save.
* \return True on success, otherwise false.
*/
bool initCodecParameters(CWMIStrCodecParam *wmiSCP, const QImage &image)
{
if (wmiSCP == nullptr || image.isNull()) {
return false;
}
memset(wmiSCP, 0, sizeof(CWMIStrCodecParam));
auto fmt = image.format();
wmiSCP->bVerbose = FALSE;
if (fmt == QImage::Format_Grayscale8 || fmt == QImage::Format_Grayscale16 || fmt == QImage::Format_Mono)
wmiSCP->cfColorFormat = Y_ONLY;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
else if (fmt == QImage::Format_CMYK8888)
wmiSCP->cfColorFormat = CMYK;
#endif
else
wmiSCP->cfColorFormat = YUV_444;
wmiSCP->bdBitDepth = BD_LONG;
wmiSCP->bfBitstreamFormat = FREQUENCY;
wmiSCP->bProgressiveMode = TRUE;
wmiSCP->olOverlap = OL_ONE;
wmiSCP->cNumOfSliceMinus1H = wmiSCP->cNumOfSliceMinus1V = 0;
wmiSCP->sbSubband = SB_ALL;
wmiSCP->uAlphaMode = image.hasAlphaChannel() ? 2 : 0;
if (quality() > -1) {
wmiSCP->uiDefaultQPIndex = qBound(0, 100 - quality(), 100);
}
return true;
}
/*!
* \brief updateTextMetadata
* Read the metadata from the image and set it in the encoder.
* \param image The image to save.
*/
void updateTextMetadata(const QImage &image)
{
if (pEncoder == nullptr) {
return;
}
DESCRIPTIVEMETADATA meta;
memset(&meta, 0, sizeof(meta));
#define META_CTEXT(name, field) \
auto field = image.text(QStringLiteral(name)).toUtf8(); \
if (!field.isEmpty()) { \
meta.field.vt = DPKVT_LPSTR; \
meta.field.VT.pszVal = field.data(); \
}
#define META_WTEXT(name, field) \
auto field = image.text(QStringLiteral(name)); \
if (!field.isEmpty()) { \
meta.field.vt = DPKVT_LPWSTR; \
meta.field.VT.pwszVal = const_cast<quint16 *>(field.utf16()); \
}
META_CTEXT(META_KEY_DESCRIPTION, pvarImageDescription)
META_CTEXT(META_KEY_MANUFACTURER, pvarCameraMake)
META_CTEXT(META_KEY_MODEL, pvarCameraModel)
META_CTEXT(META_KEY_AUTHOR, pvarArtist)
META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright)
META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
META_WTEXT(META_KEY_TITLE, pvarCaption)
#undef META_CTEXT
#undef META_WTEXT
// Software must be updated
auto software = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
if (!software.isEmpty()) {
meta.pvarSoftware.vt = DPKVT_LPSTR;
meta.pvarSoftware.VT.pszVal = software.data();
}
// Date and Time (TIFF format)
auto cDate = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
auto sDate = cDate.isValid() ? cDate.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toLatin1() : QByteArray();
if (!sDate.isEmpty()) {
meta.pvarDateTime.vt = DPKVT_LPSTR;
meta.pvarDateTime.VT.pszVal = sDate.data();
}
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
if (!xmp.isNull()) {
if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(xmp.constData()), xmp.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err;
}
}
auto exif = MicroExif::fromImage(image);
if (!exif.isEmpty()) {
auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian, MicroExif::V2);
if (auto err = PKImageEncode_SetEXIFMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(exifIfd.constData()), exifIfd.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting EXIF data:" << err;
}
auto gpsIfd = exif.gpsIfdByteArray(QDataStream::LittleEndian, MicroExif::V2);
if (auto err = PKImageEncode_SetGPSInfoMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(gpsIfd.constData()), gpsIfd.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting GPS data:" << err;
}
}
if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err;
}
}
/*!
* \brief exactFormat
* JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
* \param jxrFormat Format to be converted.
* \return A valid Qt format or QImage::Format_Invalid if there is no match
*/
static QImage::Format exactFormat(const PKPixelFormatGUID &jxrFormat)
{
auto l = exactMatchingFormats();
for (auto &&p : l) {
if (IsEqualGUID(p.second, jxrFormat))
return p.first;
}
return QImage::Format_Invalid;
}
/*!
* \brief exactFormat
* JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
* \param qtFormat Format to be converted.
* \return A valid JXR format or GUID_PKPixelFormatUndefined if there is no match
*/
static PKPixelFormatGUID exactFormat(const QImage::Format &qtFormat)
{
auto l = exactMatchingFormats();
for (auto &&p : l) {
if (p.first == qtFormat)
return p.second;
}
return GUID_PKPixelFormatUndefined;
}
private:
static QList<std::pair<QImage::Format, PKPixelFormatGUID>> exactMatchingFormats()
{
// clang-format off
auto list = QList<std::pair<QImage::Format, PKPixelFormatGUID>>()
#ifndef JXR_DENY_FLOAT_IMAGE
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA16FPx4, GUID_PKPixelFormat64bppRGBAHalf)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX16FPx4, GUID_PKPixelFormat64bppRGBHalf)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4, GUID_PKPixelFormat128bppRGBAFloat)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4_Premultiplied, GUID_PKPixelFormat128bppPRGBAFloat)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX32FPx4, GUID_PKPixelFormat128bppRGBFloat)
#endif // JXR_DENY_FLOAT_IMAGE
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYK)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYKDIRECT)
#endif
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Mono, GUID_PKPixelFormatBlackWhite)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale8, GUID_PKPixelFormat8bppGray)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale16, GUID_PKPixelFormat16bppGray)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB555, GUID_PKPixelFormat16bppRGB555)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB16, GUID_PKPixelFormat16bppRGB565)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_BGR888, GUID_PKPixelFormat24bppBGR)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB888, GUID_PKPixelFormat24bppRGB)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX8888, GUID_PKPixelFormat32bppRGB)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888, GUID_PKPixelFormat32bppRGBA)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888_Premultiplied, GUID_PKPixelFormat32bppPRGBA)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64, GUID_PKPixelFormat64bppRGBA)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64_Premultiplied, GUID_PKPixelFormat64bppPRGBA);
// clang-format on
return list;
}
bool deviceCopy(QIODevice *target, QIODevice *source)
{
if (target == nullptr || source == nullptr) {
return false;
}
auto isTargetOpen = target->isOpen();
if (!isTargetOpen && !target->open(QIODevice::WriteOnly)) {
return false;
}
auto isSourceOpen = source->isOpen();
if (!isSourceOpen && !source->open(QIODevice::ReadOnly)) {
return false;
}
QByteArray buff(32768 * 4, char());
for (;;) {
auto read = source->read(buff.data(), buff.size());
if (read == 0) {
break;
}
if (read < 0) {
return false;
}
if (target->write(buff.data(), read) != read) {
return false;
}
}
if (!isSourceOpen) {
source->close();
}
if (!isTargetOpen) {
target->close();
}
return true;
}
bool readDevice(QIODevice *device)
{
if (device == nullptr) {
return false;
}
if (!m_jxrFile.isNull()) {
return true;
}
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
QSharedPointer<QFile> file(new QFile(fileName));
if (!file->open(QFile::WriteOnly)) {
return false;
}
if (!deviceCopy(file.data(), device)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::readDevice() error while writing in the target device";
return false;
}
file->close();
m_exif = MicroExif::fromDevice(file.data());
m_jxrFile = file;
return true;
}
bool initDecoder()
{
if (pDecoder) {
return true;
}
if (pCodecFactory == nullptr) {
return false;
}
if (auto err = pCodecFactory->CreateDecoderFromFile(qUtf8Printable(fileName()), &pDecoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initDecoder() unable to create decoder:" << err;
return false;
}
setTransformation(JXRHandlerPrivate::orientationToTransformation(pDecoder->WMP.wmiI.oOrientation));
pDecoder->WMP.wmiI.oOrientation = O_NONE; // disable the library rotation application
return true;
}
bool initEncoder()
{
if (pDecoder) {
return true;
}
if (pCodecFactory == nullptr) {
return false;
}
if (auto err = pCodecFactory->CreateCodec(&IID_PKImageWmpEncode, (void **)&pEncoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initEncoder() unable to create encoder:" << err;
return false;
}
pEncoder->WMP.oOrientation = JXRHandlerPrivate::transformationToOrientation(transformation());
return true;
}
bool readTextMeta() const {
if (pDecoder == nullptr) {
return false;
}
if (!m_txtMeta.isEmpty()) {
return true;
}
DESCRIPTIVEMETADATA meta;
if (pDecoder->GetDescriptiveMetadata(pDecoder, &meta)) {
return false;
}
#define META_TEXT(name, field) \
if (meta.field.vt == DPKVT_LPSTR) \
m_txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \
else if (meta.field.vt == DPKVT_LPWSTR) \
m_txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));
META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription)
META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake)
META_TEXT(META_KEY_MODEL, pvarCameraModel)
META_TEXT(META_KEY_SOFTWARE, pvarSoftware)
META_TEXT(META_KEY_AUTHOR, pvarArtist)
META_TEXT(META_KEY_COPYRIGHT, pvarCopyright)
META_TEXT(META_KEY_TITLE, pvarCaption)
META_TEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
META_TEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
#undef META_TEXT
return true;
}
};
bool JXRHandler::read(QImage *outImage)
{
if (!d->initForReading(device())) {
return false;
}
PKPixelFormatGUID convFmt;
auto imageFmt = d->imageFormat(&convFmt);
auto img = imageAlloc(d->imageSize(), imageFmt);
if (img.isNull()) {
return false;
}
// resolution
float hres, vres;
if (auto err = d->pDecoder->GetResolution(d->pDecoder, &hres, &vres)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while reading resolution:" << err;
} else {
img.setDotsPerMeterX(qRound(hres * 1000 / 25.4));
img.setDotsPerMeterY(qRound(vres * 1000 / 25.4));
}
// alpha copy mode
if (img.hasAlphaChannel()) {
d->pDecoder->WMP.wmiSCP.uAlphaMode = 2; // or 1 (?)
}
PKRect rect = {0, 0, img.width(), img.height()};
if (IsEqualGUID(convFmt, GUID_PKPixelFormatUndefined)) { // direct storing
if (auto err = d->pDecoder->Copy(d->pDecoder, &rect, img.bits(), img.bytesPerLine())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy data:" << err;
return false;
}
} else { // conversion to a known format
PKFormatConverter *pConverter = nullptr;
if (auto err = d->pCodecFactory->CreateFormatConverter(&pConverter)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to create the converter:" << err;
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;
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;
return false;
}
} else { // additional buffer needed
qint64 convStrideSize = (img.width() * d->pDecoder->WMP.wmiI.cBitsPerUnit + 7) / 8;
qint64 buffSize = convStrideSize * img.height();
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";
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;
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);
}
// Metadata (e.g.: icc profile, description, etc...)
img.setColorSpace(d->colorSpace());
d->setMetadata(img);
#ifndef JXR_DENY_FLOAT_IMAGE
// JXR float are stored in scRGB.
if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied ||
img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4 || img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
auto hasAlpha = img.hasAlphaChannel();
for (qint32 y = 0, h = img.height(); y < h; ++y) {
if (img.depth() == 64) {
auto line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
for (int x = 0, w = img.width() * 4; x < w; x += 4)
line[x + 3] = hasAlpha ? std::clamp(line[x + 3], qfloat16(0), qfloat16(1)) : qfloat16(1);
} else {
auto line = reinterpret_cast<float *>(img.scanLine(y));
for (int x = 0, w = img.width() * 4; x < w; x += 4)
line[x + 3] = hasAlpha ? std::clamp(line[x + 3], float(0), float(1)) : float(1);
}
}
if (!img.colorSpace().isValid()) {
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
}
#endif
*outImage = img;
return true;
}
bool JXRHandler::write(const QImage &image)
{
// JXR is stored in a TIFF V6 container that is limited to 4GiB. The size
// is limited to 4GB to leave room for IFDs, Metadata, etc...
if (qint64(image.sizeInBytes()) > 4000000000ll) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() image too large: the image cannot exceed 4GB.";
return false;
}
if (!d->initForWriting()) {
return false;
}
struct WMPStream *pEncodeStream = nullptr;
if (auto err = d->pFactory->CreateStreamFromFilename(&pEncodeStream, qUtf8Printable(d->fileName()), "wb")) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() unable to create stream:" << err;
return false;
}
// convert the image to a supported format
auto qi = d->imageToSave(image);
auto jxlfmt = d->exactFormat(qi.format());
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormatUndefined)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating the target format for" << qi.format();
return false;
}
#ifndef JXR_DISABLE_BGRA_HACK
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGB)) {
jxlfmt = GUID_PKPixelFormat32bppBGR;
qi.rgbSwap();
}
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGBA)) {
jxlfmt = GUID_PKPixelFormat32bppBGRA;
qi.rgbSwap();
}
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppPRGBA)) {
jxlfmt = GUID_PKPixelFormat32bppPBGRA;
qi.rgbSwap();
}
#endif
// initialize the codec parameters
CWMIStrCodecParam wmiSCP;
if (!d->initCodecParameters(&wmiSCP, qi)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating encoder parameters for" << qi.format();
return false;
}
if (auto err = d->pEncoder->Initialize(d->pEncoder, pEncodeStream, &wmiSCP, sizeof(wmiSCP))) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while initializing the encoder:" << err;
return false;
}
// setting mandatory image info
if (auto err = d->pEncoder->SetPixelFormat(d->pEncoder, jxlfmt)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image format:" << err;
return false;
}
if (auto err = d->pEncoder->SetSize(d->pEncoder, qi.width(), qi.height())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image size:" << err;
return false;
}
if (auto err = d->pEncoder->SetResolution(d->pEncoder, qi.dotsPerMeterX() * 25.4 / 1000, qi.dotsPerMeterY() * 25.4 / 1000)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image resolution:" << err;
return false;
}
// setting metadata (a failure of setting metadata doesn't stop the encoding)
auto cs = qi.colorSpace().iccProfile();
if (!cs.isEmpty()) {
if (auto err = d->pEncoder->SetColorContext(d->pEncoder, reinterpret_cast<quint8 *>(cs.data()), cs.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting ICC profile:" << err;
}
}
d->updateTextMetadata(image);
// writing the image
if (auto err = d->pEncoder->WritePixels(d->pEncoder, qi.height(), qi.bits(), qi.bytesPerLine())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while encoding the image:" << err;
return false;
}
if (!d->finalizeWriting(device())) {
return false;
}
return true;
}
void JXRHandler::setOption(ImageOption option, const QVariant &value)
{
if (option == QImageIOHandler::Quality) {
auto ok = false;
auto q = value.toInt(&ok);
if (ok) {
d->setQuality(q);
}
}
if (option == QImageIOHandler::ImageTransformation) {
auto ok = false;
auto t = value.toInt(&ok);
if (ok) {
d->setTransformation(QImageIOHandler::Transformation(t));
}
}
}
bool JXRHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
if (option == QImageIOHandler::Quality) {
return true;
}
if (option == QImageIOHandler::ImageTransformation) {
return true;
}
return false;
}
QVariant JXRHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
if (d->initForReadingAndRollBack(device())) {
auto size = d->imageSize();
if (size.isValid()) {
v = QVariant::fromValue(size);
}
}
}
if (option == QImageIOHandler::ImageFormat) {
if (d->initForReadingAndRollBack(device())) {
v = QVariant::fromValue(d->imageFormat());
}
}
if (option == QImageIOHandler::Quality) {
v = d->quality();
}
if (option == QImageIOHandler::ImageTransformation) {
// ignore result: I might want to read the value set in writing
d->initForReadingAndRollBack(device());
v = int(d->transformation());
}
return v;
}
JXRHandler::JXRHandler()
: d(new JXRHandlerPrivate)
{
}
bool JXRHandler::canRead() const
{
if (canRead(device())) {
setFormat("jxr");
return true;
}
return false;
}
bool JXRHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::canRead() called with no device";
return false;
}
// JPEG XR image data is stored in TIFF-like container format (II and 0xBC01 version)
if (device->peek(4) == QByteArray::fromRawData("\x49\x49\xbc\x01", 4)) {
return true;
}
return false;
}
QImageIOPlugin::Capabilities JXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "jxr") {
return Capabilities(CanRead | CanWrite);
}
if (format == "wdp" || format == "hdp") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && JXRHandler::canRead(device)) {
cap |= CanRead;
}
if (device->isWritable()) {
cap |= CanWrite;
}
return cap;
}
QImageIOHandler *JXRPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new JXRHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_jxr_p.cpp"