KImageFormats

jxr.cpp
1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8/*
9 * Info about JXR:
10 * - https://learn.microsoft.com/en-us/windows/win32/wic/jpeg-xr-codec
11 *
12 * Sample images:
13 * - http://fileformats.archiveteam.org/wiki/JPEG_XR
14 * - https://github.com/bvibber/hdrfix/tree/main/samples
15 */
16
17#include "jxr_p.h"
18#include "util_p.h"
19
20#include <QColorSpace>
21#include <QCoreApplication>
22#include <QDataStream>
23#include <QFile>
24#include <QFloat16>
25#include <QHash>
26#include <QImage>
27#include <QImageReader>
28#include <QLoggingCategory>
29#include <QSet>
30#include <QSharedData>
31#include <QTemporaryDir>
32
33#include <JXRGlue.h>
34#include <cstring>
35
36Q_DECLARE_LOGGING_CATEGORY(LOG_JXRPLUGIN)
37Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
38
39/*!
40 * Support for float images
41 *
42 * NOTE: Float images have values greater than 1 so they need an additional in place conversion.
43 */
44// #define JXR_DENY_FLOAT_IMAGE
45
46/*!
47 * Remove the neeeds of additional memory by disabling the conversion between
48 * different color depths (e.g. RGBA64bpp to RGBA32bpp).
49 *
50 * NOTE: Leaving deptch conversion enabled (default) ensures maximum read compatibility.
51 */
52// #define JXR_DISABLE_DEPTH_CONVERSION // default commented
53
54/*!
55 * Windows displays and opens JXR files correctly out of the box. Unfortunately it doesn't
56 * seem to open (P)RGBA @32bpp files as it only wants (P)BGRA32bpp files (a format not supported by Qt).
57 * Only for this format an hack is activated to guarantee total compatibility of the plugin with Windows.
58 */
59// #define JXR_DISABLE_BGRA_HACK // default commented
60
61/*!
62 * 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.
63 *
64 * - ERR PKImageDecode_GetXMPMetadata_WMP(PKImageDecode *pID, U8 *pbXMPMetadata, U32 *pcbXMPMetadata);
65 * - ERR PKImageDecode_GetEXIFMetadata_WMP(PKImageDecode *pID, U8 *pbEXIFMetadata, U32 *pcbEXIFMetadata);
66 * - ERR PKImageDecode_GetGPSInfoMetadata_WMP(PKImageDecode *pID, U8 *pbGPSInfoMetadata, U32 *pcbGPSInfoMetadata);
67 * - ERR PKImageDecode_GetIPTCNAAMetadata_WMP(PKImageDecode *pID, U8 *pbIPTCNAAMetadata, U32 *pcbIPTCNAAMetadata);
68 * - ERR PKImageDecode_GetPhotoshopMetadata_WMP(PKImageDecode *pID, U8 *pbPhotoshopMetadata, U32 *pcbPhotoshopMetadata);
69 *
70 * As a result, their use is disabled by default. It is possible to activate their use by defining the
71 * JXR_ENABLE_ADVANCED_METADATA preprocessor directive
72 */
73
74// #define JXR_ENABLE_ADVANCED_METADATA
75
76class JXRHandlerPrivate : public QSharedData
77{
78private:
80 mutable QSharedPointer<QFile> jxrFile;
81 mutable QHash<QString, QString> txtMeta;
82
83public:
84 PKFactory *pFactory = nullptr;
85 PKCodecFactory *pCodecFactory = nullptr;
86 PKImageDecode *pDecoder = nullptr;
87 PKImageEncode *pEncoder = nullptr;
88
89 JXRHandlerPrivate()
90 {
92 if (PKCreateFactory(&pFactory, PK_SDK_VERSION) == WMP_errSuccess) {
93 PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION);
94 }
95 if (pFactory == nullptr || pCodecFactory == nullptr) {
96 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() initialization error of JXR library!";
97 }
98 }
99 JXRHandlerPrivate(const JXRHandlerPrivate &other) = default;
100
101 ~JXRHandlerPrivate()
102 {
103 if (pCodecFactory) {
104 PKCreateCodecFactory_Release(&pCodecFactory);
105 }
106 if (pFactory) {
107 PKCreateFactory_Release(&pFactory);
108 }
109 if (pDecoder) {
110 PKImageDecode_Release(&pDecoder);
111 }
112 if (pEncoder) {
113 PKImageEncode_Release(&pEncoder);
114 }
115 }
116
117 QString fileName() const
118 {
119 return jxrFile->fileName();
120 }
121
122 /* *** READ *** */
123
124 /*!
125 * \brief initForReading
126 * Initialize the device for reading.
127 * \param device The source device.
128 * \return True on success, otherwise false.
129 */
130 bool initForReading(QIODevice *device)
131 {
132 if (!readDevice(device)) {
133 return false;
134 }
135 if (!initDecoder()) {
136 return false;
137 }
138 return true;
139 }
140
141 /*!
142 * \brief jxrFormat
143 * \return The JXR format.
144 */
145 PKPixelFormatGUID jxrFormat() const
146 {
147 PKPixelFormatGUID pixelFormatGUID = GUID_PKPixelFormatUndefined;
148 if (pDecoder) {
149 pDecoder->GetPixelFormat(pDecoder, &pixelFormatGUID);
150 }
151 return pixelFormatGUID;
152 }
153
154 /*!
155 * \brief imageFormat
156 * 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.
157 * \return The QImage format. If invalid, the image cannot be read.
158 */
159 QImage::Format imageFormat(PKPixelFormatGUID *conversionFormat = nullptr) const
160 {
161 PKPixelFormatGUID tmp;
162 if (conversionFormat == nullptr) {
163 conversionFormat = &tmp;
164 }
165 *conversionFormat = GUID_PKPixelFormatUndefined;
166
167 auto jxrfmt = jxrFormat();
168 auto qtFormat = exactFormat(jxrfmt);
169 if (qtFormat != QImage::Format_Invalid) {
170 return qtFormat;
171 }
172
173 // *** CONVERSION WITH THE SAME DEPTH ***
174 // IMPORTANT: For supported conversions see JXRGluePFC.c
175
176 // 32-bit
177 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGR)) {
178 *conversionFormat = GUID_PKPixelFormat24bppRGB;
180 };
181 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGRA)) {
182 *conversionFormat = GUID_PKPixelFormat32bppRGBA;
184 };
185 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppPBGRA)) {
186 *conversionFormat = GUID_PKPixelFormat32bppPRGBA;
188 };
189
190#ifndef JXR_DENY_FLOAT_IMAGE
191 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFixedPoint)) {
192 *conversionFormat = GUID_PKPixelFormat128bppRGBAFloat;
194 };
195#endif // !JXR_DENY_FLOAT_IMAGE
196
197 // *** CONVERSION TO A LOWER DEPTH ***
198 // IMPORTANT: For supported conversions see JXRGluePFC.c
199
200#ifndef JXR_DISABLE_DEPTH_CONVERSION
201
202#ifndef JXR_DENY_FLOAT_IMAGE
203 // RGB FLOAT
204 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFloat)) {
205 *conversionFormat = GUID_PKPixelFormat64bppRGBHalf;
207 };
208#endif // !JXR_DENY_FLOAT_IMAGE
209
210 // RGBA
211 // clang-format off
212 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBAHalf) ||
213 IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBAFixedPoint) ||
214 IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFixedPoint) ||
215 IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFloat)) {
216
217 *conversionFormat = GUID_PKPixelFormat32bppRGBA;
219 };
220 // clang-format on
221
222 // RGB
223 // clang-format off
224 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBFloat) ||
225 IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFloat) ||
226 IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBFixedPoint) ||
227 IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFixedPoint) ||
228 IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBFixedPoint) ||
229 IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGBHalf) ||
230 IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBHalf) ||
231 IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGBFixedPoint) ||
232 IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppRGB101010) ||
233 IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGB) ||
234 IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppRGBE) ) {
235
236 *conversionFormat = GUID_PKPixelFormat24bppRGB;
238 };
239 // clang-format on
240
241 // Gray
242 // clang-format off
243 if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppGrayFloat) ||
244 IsEqualGUID(jxrfmt, GUID_PKPixelFormat16bppGrayFixedPoint) ||
245 IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppGrayFixedPoint) ||
246 IsEqualGUID(jxrfmt, GUID_PKPixelFormat16bppGrayHalf)) {
247
248 *conversionFormat = GUID_PKPixelFormat8bppGray;
250 };
251 // clang-format on
252#endif // !JXR_DISABLE_DEPTH_CONVERSION
253
255 }
256
257 /*!
258 * \brief imageSize
259 * \return The image size in pixels.
260 */
261 QSize imageSize() const
262 {
263 if (pDecoder) {
264 qint32 w, h;
265 pDecoder->GetSize(pDecoder, &w, &h);
266 return QSize(w, h);
267 }
268 return {};
269 }
270
271 /*!
272 * \brief colorSpace
273 * \return The ICC profile if exists.
274 */
275 QColorSpace colorSpace() const
276 {
277 QColorSpace cs;
278 if (pDecoder == nullptr) {
279 return cs;
280 }
281 quint32 size;
282 if (!pDecoder->GetColorContext(pDecoder, nullptr, &size) && size) {
283 QByteArray ba(size, 0);
284 if (!pDecoder->GetColorContext(pDecoder, reinterpret_cast<quint8 *>(ba.data()), &size)) {
286 }
287 }
288 return cs;
289 }
290
291 /*!
292 * \brief xmpData
293 * \return The XMP data if exists.
294 */
295 QString xmpData() const
296 {
297 QString xmp;
298 if (pDecoder == nullptr) {
299 return xmp;
300 }
301#ifdef JXR_ENABLE_ADVANCED_METADATA
302 quint32 size;
303 if (!PKImageDecode_GetXMPMetadata_WMP(pDecoder, nullptr, &size) && size) {
304 QByteArray ba(size, 0);
305 if (!PKImageDecode_GetXMPMetadata_WMP(pDecoder, reinterpret_cast<quint8 *>(ba.data()), &size)) {
306 xmp = QString::fromUtf8(ba);
307 }
308 }
309#endif
310 return xmp;
311 }
312
313 /*!
314 * \brief setTextMetadata
315 * Set the text metadata into \a image
316 * \param image Image on which to write metadata
317 */
318 void setTextMetadata(QImage& image)
319 {
320 auto xmp = xmpData();
321 if (!xmp.isEmpty()) {
322 image.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp);
323 }
324 auto descr = description();
325 if (!descr.isEmpty()) {
326 image.setText(QStringLiteral(META_KEY_DESCRIPTION), descr);
327 }
328 auto softw = software();
329 if (!softw.isEmpty()) {
330 image.setText(QStringLiteral(META_KEY_SOFTWARE), softw);
331 }
332 auto make = cameraMake();
333 if (!make.isEmpty()) {
334 image.setText(QStringLiteral(META_KEY_MANUFACTURER), make);
335 }
336 auto model = cameraModel();
337 if (!model.isEmpty()) {
338 image.setText(QStringLiteral(META_KEY_MODEL), model);
339 }
340 auto cDate = dateTime();
341 if (!cDate.isEmpty()) {
342 image.setText(QStringLiteral(META_KEY_CREATIONDATE), cDate);
343 }
344 auto author = artist();
345 if (!author.isEmpty()) {
346 image.setText(QStringLiteral(META_KEY_AUTHOR), author);
347 }
348 auto copy = copyright();
349 if (!copy.isEmpty()) {
350 image.setText(QStringLiteral(META_KEY_COPYRIGHT), copy);
351 }
352 auto capt = caption();
353 if (!capt.isEmpty()) {
354 image.setText(QStringLiteral(META_KEY_TITLE), capt);
355 }
356 auto host = hostComputer();
357 if (!host.isEmpty()) {
358 image.setText(QStringLiteral(META_KEY_HOSTCOMPUTER), capt);
359 }
360 auto docn = documentName();
361 if (!docn.isEmpty()) {
362 image.setText(QStringLiteral(META_KEY_DOCUMENTNAME), docn);
363 }
364 }
365
366#define META_TEXT(name, key) \
367 QString name() const \
368 { \
369 readTextMeta(); \
370 return txtMeta.value(QStringLiteral(key)); \
371 }
372
373 META_TEXT(description, META_KEY_DESCRIPTION)
374 META_TEXT(cameraMake, META_KEY_MANUFACTURER)
375 META_TEXT(cameraModel, META_KEY_MODEL)
376 META_TEXT(software, META_KEY_SOFTWARE)
377 META_TEXT(dateTime, META_KEY_CREATIONDATE)
378 META_TEXT(artist, META_KEY_AUTHOR)
379 META_TEXT(copyright, META_KEY_COPYRIGHT)
380 META_TEXT(caption, META_KEY_TITLE)
381 META_TEXT(documentName, META_KEY_DOCUMENTNAME)
382 META_TEXT(hostComputer, META_KEY_HOSTCOMPUTER)
383
384#undef META_TEXT
385
386 /* *** WRITE *** */
387
388 /*!
389 * \brief initForWriting
390 * Initialize the stream for writing.
391 * \return True on success, otherwise false.
392 */
393 bool initForWriting()
394 {
395 // I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
396 auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
397 QSharedPointer<QFile> file(new QFile(fileName));
398 jxrFile = file;
399 return initEncoder();
400 }
401
402 /*!
403 * \brief finalizeWriting
404 * \param device
405 * Finalize the writing operation. Must be called as last peration.
406 * \return True on success, otherwise false.
407 */
408 bool finalizeWriting(QIODevice *device)
409 {
410 if (device == nullptr || pEncoder == nullptr) {
411 return false;
412 }
413 if (auto err = PKImageEncode_Release(&pEncoder)) {
414 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while releasing the encoder:" << err;
415 return false;
416 }
417
418 if (!deviceCopy(device, jxrFile.data())) {
419 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while writing in the target device";
420 return false;
421 }
422 return true;
423 }
424
425 /*!
426 * \brief imageToSave
427 * If necessary it converts the image to be saved into the appropriate format otherwise it does nothing.
428 * \param source The image to save.
429 * \return The image to use for save operation.
430 */
431 QImage imageToSave(const QImage &source) const
432 {
433 // IMPORTANT: these values must be in exactMatchingFormat()
434 // clang-format off
435 auto valid = QSet<QImage::Format>()
436#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
437 << QImage::Format_CMYK8888
438#endif
439#ifndef JXR_DENY_FLOAT_IMAGE
445#endif // JXR_DENY_FLOAT_IMAGE
458 // clang-format on
459
460 // To avoid complex code, I will save only integer formats.
461 auto qi = source;
462 if (qi.format() == QImage::Format_MonoLSB) {
463 qi = qi.convertToFormat(QImage::Format_Mono);
464 }
465 if (qi.format() == QImage::Format_Indexed8) {
466 if (qi.allGray())
467 qi = qi.convertToFormat(QImage::Format_Grayscale8);
468 else
469 qi = qi.convertToFormat(QImage::Format_RGBA8888);
470 }
471#ifndef JXR_DENY_FLOAT_IMAGE
472 if (qi.format() == QImage::Format_RGBA16FPx4_Premultiplied) {
473 qi = qi.convertToFormat(QImage::Format_RGBA16FPx4);
474 }
475#endif // JXR_DENY_FLOAT_IMAGE
476
477 // generic
478 if (!valid.contains(qi.format())) {
479 auto alpha = qi.hasAlphaChannel();
480 auto depth = qi.depth();
481 if (depth >= 12 && depth <= 24 && !alpha) {
482 qi = qi.convertToFormat(QImage::Format_RGB888);
483 } else if (depth >= 48) {
484 // JXR don't have RGBX64 format so I have two possibilities:
485 // - convert to 32 bpp (convertToFormat(alpha ? QImage::Format_RGBA64 : QImage::Format_RGB888))
486 // - convert to 64 bpp with fake alpha (preferred)
487 qi = qi.convertToFormat(QImage::Format_RGBA64);
488 } else {
489 qi = qi.convertToFormat(alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
490 }
491#ifndef JXR_DENY_FLOAT_IMAGE
492 // clang-format off
493 } else if (qi.format() == QImage::Format_RGBA16FPx4 ||
494 qi.format() == QImage::Format_RGBX16FPx4 ||
495 qi.format() == QImage::Format_RGBA32FPx4 ||
497 qi.format() == QImage::Format_RGBX32FPx4) {
498 // clang-format on
499 auto cs = qi.colorSpace();
500 if (cs.isValid() && cs.transferFunction() != QColorSpace::TransferFunction::Linear) {
501 qi = qi.convertedToColorSpace(QColorSpace(QColorSpace::SRgbLinear));
502 }
503 }
504#endif // JXR_DENY_FLOAT_IMAGE
505
506 return qi;
507 }
508
509 /*!
510 * \brief initCodecParameters
511 * Initialize the JXR codec parameters.
512 * \param wmiSCP
513 * \param image The image to save.
514 * \return True on success, otherwise false.
515 */
516 bool initCodecParameters(CWMIStrCodecParam *wmiSCP, const QImage &image)
517 {
518 if (wmiSCP == nullptr || image.isNull()) {
519 return false;
520 }
521 memset(wmiSCP, 0, sizeof(CWMIStrCodecParam));
522
523 auto fmt = image.format();
524
525 wmiSCP->bVerbose = FALSE;
527 wmiSCP->cfColorFormat = Y_ONLY;
528#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
529 else if (fmt == QImage::Format_CMYK8888)
530 wmiSCP->cfColorFormat = CMYK;
531#endif
532 else
533 wmiSCP->cfColorFormat = YUV_444;
534 wmiSCP->bdBitDepth = BD_LONG;
535 wmiSCP->bfBitstreamFormat = FREQUENCY;
536 wmiSCP->bProgressiveMode = TRUE;
537 wmiSCP->olOverlap = OL_ONE;
538 wmiSCP->cNumOfSliceMinus1H = wmiSCP->cNumOfSliceMinus1V = 0;
539 wmiSCP->sbSubband = SB_ALL;
540 wmiSCP->uAlphaMode = image.hasAlphaChannel() ? 2 : 0;
541 return true;
542 }
543
544 /*!
545 * \brief updateTextMetadata
546 * Read the metadata from the image and set it in the encoder.
547 * \param image The image to save.
548 */
549 void updateTextMetadata(const QImage &image)
550 {
551 if (pEncoder == nullptr) {
552 return;
553 }
554
555 DESCRIPTIVEMETADATA meta;
556 memset(&meta, 0, sizeof(meta));
557
558#define META_CTEXT(name, field) \
559 auto field = image.text(QStringLiteral(name)).toUtf8(); \
560 if (!field.isEmpty()) { \
561 meta.field.vt = DPKVT_LPSTR; \
562 meta.field.VT.pszVal = field.data(); \
563 }
564#define META_WTEXT(name, field) \
565 auto field = image.text(QStringLiteral(name)); \
566 if (!field.isEmpty()) { \
567 meta.field.vt = DPKVT_LPWSTR; \
568 meta.field.VT.pwszVal = const_cast<quint16 *>(field.utf16()); \
569 }
570
571 META_CTEXT(META_KEY_DESCRIPTION, pvarImageDescription)
572 META_CTEXT(META_KEY_MANUFACTURER, pvarCameraMake)
573 META_CTEXT(META_KEY_MODEL, pvarCameraModel)
574 META_CTEXT(META_KEY_AUTHOR, pvarArtist)
575 META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright)
576 META_CTEXT(META_KEY_CREATIONDATE, pvarDateTime)
577 META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
578 META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
579 META_WTEXT(META_KEY_TITLE, pvarCaption)
580
581#undef META_CTEXT
582#undef META_WTEXT
583
584 // Software must be updated
585 auto software = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
586 if (!software.isEmpty()) {
587 meta.pvarSoftware.vt = DPKVT_LPSTR;
588 meta.pvarSoftware.VT.pszVal = software.data();
589 }
590
591 auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
592 if (!xmp.isNull()) {
593 if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<quint8 *>(xmp.data()), xmp.size())) {
594 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err;
595 }
596 }
597 if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) {
598 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err;
599 }
600 }
601
602 /*!
603 * \brief exactFormat
604 * JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
605 * \param jxrFormat Format to be converted.
606 * \return A valid Qt format or QImage::Format_Invalid if there is no match
607 */
608 static QImage::Format exactFormat(const PKPixelFormatGUID &jxrFormat)
609 {
610 auto l = exactMatchingFormats();
611 for (auto &&p : l) {
612 if (IsEqualGUID(p.second, jxrFormat))
613 return p.first;
614 }
616 }
617
618 /*!
619 * \brief exactFormat
620 * JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
621 * \param qtFormat Format to be converted.
622 * \return A valid JXR format or GUID_PKPixelFormatUndefined if there is no match
623 */
624 static PKPixelFormatGUID exactFormat(const QImage::Format &qtFormat)
625 {
626 auto l = exactMatchingFormats();
627 for (auto &&p : l) {
628 if (p.first == qtFormat)
629 return p.second;
630 }
631 return GUID_PKPixelFormatUndefined;
632 }
633
634private:
635 static QList<std::pair<QImage::Format, PKPixelFormatGUID>> exactMatchingFormats()
636 {
637 // clang-format off
639#ifndef JXR_DENY_FLOAT_IMAGE
640 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA16FPx4, GUID_PKPixelFormat64bppRGBAHalf)
641 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX16FPx4, GUID_PKPixelFormat64bppRGBHalf)
642 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4, GUID_PKPixelFormat128bppRGBAFloat)
643 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4_Premultiplied, GUID_PKPixelFormat128bppPRGBAFloat)
644 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX32FPx4, GUID_PKPixelFormat128bppRGBFloat)
645#endif // JXR_DENY_FLOAT_IMAGE
646#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
647 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYK)
648 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYKDIRECT)
649#endif
650 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Mono, GUID_PKPixelFormatBlackWhite)
651 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale8, GUID_PKPixelFormat8bppGray)
652 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale16, GUID_PKPixelFormat16bppGray)
653 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB555, GUID_PKPixelFormat16bppRGB555)
654 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB16, GUID_PKPixelFormat16bppRGB565)
655 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_BGR888, GUID_PKPixelFormat24bppBGR)
656 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB888, GUID_PKPixelFormat24bppRGB)
657 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX8888, GUID_PKPixelFormat32bppRGB)
658 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888, GUID_PKPixelFormat32bppRGBA)
659 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888_Premultiplied, GUID_PKPixelFormat32bppPRGBA)
660 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64, GUID_PKPixelFormat64bppRGBA)
661 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64_Premultiplied, GUID_PKPixelFormat64bppPRGBA);
662 // clang-format on
663 return list;
664 }
665
666 bool deviceCopy(QIODevice *target, QIODevice *source)
667 {
668 if (target == nullptr || source == nullptr) {
669 return false;
670 }
671 auto isTargetOpen = target->isOpen();
672 if (!isTargetOpen && !target->open(QIODevice::WriteOnly)) {
673 return false;
674 }
675 auto isSourceOpen = source->isOpen();
676 if (!isSourceOpen && !source->open(QIODevice::ReadOnly)) {
677 return false;
678 }
679 QByteArray buff(32768 * 4, char());
680 for (;;) {
681 auto read = source->read(buff.data(), buff.size());
682 if (read == 0) {
683 break;
684 }
685 if (read < 0) {
686 return false;
687 }
688 if (target->write(buff.data(), read) != read) {
689 return false;
690 }
691 }
692 if (!isSourceOpen) {
693 source->close();
694 }
695 if (!isTargetOpen) {
696 target->close();
697 }
698 return true;
699 }
700
701 bool readDevice(QIODevice *device)
702 {
703 if (device == nullptr) {
704 return false;
705 }
706 if (!jxrFile.isNull()) {
707 return true;
708 }
709 // I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
710 auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
711 QSharedPointer<QFile> file(new QFile(fileName));
712 if (!file->open(QFile::WriteOnly)) {
713 return false;
714 }
715 if (!deviceCopy(file.data(), device)) {
716 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::readDevice() error while writing in the target device";
717 return false;
718 }
719 file->close();
720 jxrFile = file;
721 return true;
722 }
723
724 bool initDecoder()
725 {
726 if (pDecoder) {
727 return true;
728 }
729 if (pCodecFactory == nullptr) {
730 return false;
731 }
732 if (auto err = pCodecFactory->CreateDecoderFromFile(qUtf8Printable(fileName()), &pDecoder)) {
733 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initDecoder() unable to create decoder:" << err;
734 return false;
735 }
736 return true;
737 }
738
739 bool initEncoder()
740 {
741 if (pDecoder) {
742 return true;
743 }
744 if (pCodecFactory == nullptr) {
745 return false;
746 }
747 if (auto err = pCodecFactory->CreateCodec(&IID_PKImageWmpEncode, (void **)&pEncoder)) {
748 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initEncoder() unable to create encoder:" << err;
749 return false;
750 }
751 return true;
752 }
753
754 bool readTextMeta() const {
755 if (pDecoder == nullptr) {
756 return false;
757 }
758 if (!txtMeta.isEmpty()) {
759 return true;
760 }
761
762 DESCRIPTIVEMETADATA meta;
763 if (pDecoder->GetDescriptiveMetadata(pDecoder, &meta)) {
764 return false;
765 }
766
767#define META_TEXT(name, field) \
768 if (meta.field.vt == DPKVT_LPSTR) \
769 txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \
770 else if (meta.field.vt == DPKVT_LPWSTR) \
771 txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));
772
773 META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription)
774 META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake)
775 META_TEXT(META_KEY_MODEL, pvarCameraModel)
776 META_TEXT(META_KEY_SOFTWARE, pvarSoftware)
777 META_TEXT(META_KEY_CREATIONDATE, pvarDateTime)
778 META_TEXT(META_KEY_AUTHOR, pvarArtist)
779 META_TEXT(META_KEY_COPYRIGHT, pvarCopyright)
780 META_TEXT(META_KEY_TITLE, pvarCaption)
781 META_TEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
782 META_TEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
783
784#undef META_TEXT
785
786 return true;
787 }
788};
789
790bool JXRHandler::read(QImage *outImage)
791{
792 if (!d->initForReading(device())) {
793 return false;
794 }
795
796 PKPixelFormatGUID convFmt;
797 auto imageFmt = d->imageFormat(&convFmt);
798 auto img = imageAlloc(d->imageSize(), imageFmt);
799 if (img.isNull()) {
800 return false;
801 }
802
803 // resolution
804 float hres, vres;
805 if (auto err = d->pDecoder->GetResolution(d->pDecoder, &hres, &vres)) {
806 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while reading resolution:" << err;
807 } else {
808 img.setDotsPerMeterX(qRound(hres * 1000 / 25.4));
809 img.setDotsPerMeterY(qRound(vres * 1000 / 25.4));
810 }
811
812 // alpha copy mode
813 if (img.hasAlphaChannel()) {
814 d->pDecoder->WMP.wmiSCP.uAlphaMode = 2; // or 1 (?)
815 }
816
817 PKRect rect = {0, 0, img.width(), img.height()};
818 if (IsEqualGUID(convFmt, GUID_PKPixelFormatUndefined)) { // direct storing
819 if (auto err = d->pDecoder->Copy(d->pDecoder, &rect, img.bits(), img.bytesPerLine())) {
820 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy data:" << err;
821 return false;
822 }
823 } else { // conversion to a known format
824 PKFormatConverter *pConverter = nullptr;
825 if (auto err = d->pCodecFactory->CreateFormatConverter(&pConverter)) {
826 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to create the converter:" << err;
827 return false;
828 }
829 if (auto err = pConverter->Initialize(pConverter, d->pDecoder, nullptr, convFmt)) {
830 PKFormatConverter_Release(&pConverter);
831 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to initialize the converter:" << err;
832 return false;
833 }
834 if (d->pDecoder->WMP.wmiI.cBitsPerUnit == size_t(img.depth())) { // in place conversion
835 if (auto err = pConverter->Copy(pConverter, &rect, img.bits(), img.bytesPerLine())) {
836 PKFormatConverter_Release(&pConverter);
837 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
838 return false;
839 }
840 } else { // additional buffer needed
841 qint64 convStrideSize = (img.width() * d->pDecoder->WMP.wmiI.cBitsPerUnit + 7) / 8;
842 qint64 buffSize = convStrideSize * img.height();
843 qint64 limit = QImageReader::allocationLimit();
844 if (limit && (buffSize + img.sizeInBytes()) > limit * 1024 * 1024) {
845 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to covert due to allocation limit set:" << limit << "MiB";
846 return false;
847 }
848 QVector<quint8> ba(buffSize);
849 if (auto err = pConverter->Copy(pConverter, &rect, ba.data(), convStrideSize)) {
850 PKFormatConverter_Release(&pConverter);
851 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
852 return false;
853 }
854 for (qint32 y = 0, h = img.height(); y < h; ++y) {
855 std::memcpy(img.scanLine(y), ba.data() + convStrideSize * y, (std::min)(convStrideSize, img.bytesPerLine()));
856 }
857 }
858 PKFormatConverter_Release(&pConverter);
859 }
860
861 // Metadata (e.g.: icc profile, description, etc...)
862 img.setColorSpace(d->colorSpace());
863 d->setTextMetadata(img);
864
865#ifndef JXR_DENY_FLOAT_IMAGE
866 // JXR float are stored in scRGB.
867 if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied ||
868 img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4 || img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
869 auto hasAlpha = img.hasAlphaChannel();
870 for (qint32 y = 0, h = img.height(); y < h; ++y) {
871 if (img.depth() == 64) {
872 auto line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
873 for (int x = 0, w = img.width() * 4; x < w; x += 4)
874 line[x + 3] = hasAlpha ? std::clamp(line[x + 3], qfloat16(0), qfloat16(1)) : qfloat16(1);
875 } else {
876 auto line = reinterpret_cast<float *>(img.scanLine(y));
877 for (int x = 0, w = img.width() * 4; x < w; x += 4)
878 line[x + 3] = hasAlpha ? std::clamp(line[x + 3], float(0), float(1)) : float(1);
879 }
880 }
881 if (!img.colorSpace().isValid()) {
882 img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
883 }
884 }
885#endif
886
887 *outImage = img;
888 return true;
889}
890
891bool JXRHandler::write(const QImage &image)
892{
893 // JXR is stored in a TIFF V6 container that is limited to 4GiB. The size
894 // is limited to 4GB to leave room for IFDs, Metadata, etc...
895 if (qint64(image.sizeInBytes()) > 4000000000ll) {
896 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() image too large: the image cannot exceed 4GB.";
897 return false;
898 }
899
900 if (!d->initForWriting()) {
901 return false;
902 }
903 struct WMPStream *pEncodeStream = nullptr;
904 if (auto err = d->pFactory->CreateStreamFromFilename(&pEncodeStream, qUtf8Printable(d->fileName()), "wb")) {
905 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() unable to create stream:" << err;
906 return false;
907 }
908
909 // convert the image to a supported format
910 auto qi = d->imageToSave(image);
911 auto jxlfmt = d->exactFormat(qi.format());
912 if (IsEqualGUID(jxlfmt, GUID_PKPixelFormatUndefined)) {
913 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating the target format for" << qi.format();
914 return false;
915 }
916#ifndef JXR_DISABLE_BGRA_HACK
917 if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGBA)) {
918 jxlfmt = GUID_PKPixelFormat32bppBGRA;
919 qi.rgbSwap();
920 }
921 if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppPRGBA)) {
922 jxlfmt = GUID_PKPixelFormat32bppPBGRA;
923 qi.rgbSwap();
924 }
925#endif
926
927 // initialize the codec parameters
928 CWMIStrCodecParam wmiSCP;
929 if (!d->initCodecParameters(&wmiSCP, qi)) {
930 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating encoder parameters for" << qi.format();
931 return false;
932 }
933 if (m_quality > -1) {
934 wmiSCP.uiDefaultQPIndex = qBound(0, 100 - m_quality, 100);
935 }
936
937 if (auto err = d->pEncoder->Initialize(d->pEncoder, pEncodeStream, &wmiSCP, sizeof(wmiSCP))) {
938 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while initializing the encoder:" << err;
939 return false;
940 }
941
942 // setting mandatory image info
943 if (auto err = d->pEncoder->SetPixelFormat(d->pEncoder, jxlfmt)) {
944 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image format:" << err;
945 return false;
946 }
947 if (auto err = d->pEncoder->SetSize(d->pEncoder, qi.width(), qi.height())) {
948 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image size:" << err;
949 return false;
950 }
951 if (auto err = d->pEncoder->SetResolution(d->pEncoder, qi.dotsPerMeterX() * 25.4 / 1000, qi.dotsPerMeterY() * 25.4 / 1000)) {
952 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image resolution:" << err;
953 return false;
954 }
955
956 // setting metadata (a failure of setting metadata doesn't stop the encoding)
957 auto cs = qi.colorSpace().iccProfile();
958 if (!cs.isEmpty()) {
959 if (auto err = d->pEncoder->SetColorContext(d->pEncoder, reinterpret_cast<quint8 *>(cs.data()), cs.size())) {
960 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting ICC profile:" << err;
961 }
962 }
963 d->updateTextMetadata(image);
964
965 // writing the image
966 if (auto err = d->pEncoder->WritePixels(d->pEncoder, qi.height(), qi.bits(), qi.bytesPerLine())) {
967 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while encoding the image:" << err;
968 return false;
969 }
970 if (!d->finalizeWriting(device())) {
971 return false;
972 }
973 return true;
974}
975
976void JXRHandler::setOption(ImageOption option, const QVariant &value)
977{
978 if (option == QImageIOHandler::Quality) {
979 bool ok = false;
980 auto q = value.toInt(&ok);
981 if (ok) {
982 m_quality = q;
983 }
984 }
985}
986
987bool JXRHandler::supportsOption(ImageOption option) const
988{
989 if (option == QImageIOHandler::Size) {
990 return true;
991 }
992 if (option == QImageIOHandler::ImageFormat) {
993 return true;
994 }
995 if (option == QImageIOHandler::Quality) {
996 return true;
997 }
999 return false; // disabled because test cases are missing
1000 }
1001 return false;
1002}
1003
1004QVariant JXRHandler::option(ImageOption option) const
1005{
1006 QVariant v;
1007
1008 if (option == QImageIOHandler::Size) {
1009 if (d->initForReading(device())) {
1010 auto size = d->imageSize();
1011 if (size.isValid()) {
1012 v = QVariant::fromValue(size);
1013 }
1014 }
1015 }
1016
1017 if (option == QImageIOHandler::ImageFormat) {
1018 if (d->initForReading(device())) {
1019 v = QVariant::fromValue(d->imageFormat());
1020 }
1021 }
1022
1023 if (option == QImageIOHandler::Quality) {
1024 v = m_quality;
1025 }
1026
1028 // TODO: rotation info (test case needed)
1029 if (d->initForReading(device())) {
1030 switch (d->pDecoder->WMP.oOrientationFromContainer) {
1031 case O_FLIPV:
1033 break;
1034 case O_FLIPH:
1036 break;
1037 case O_FLIPVH:
1039 break;
1040 case O_RCW:
1042 break;
1043 case O_RCW_FLIPV:
1045 break;
1046 case O_RCW_FLIPH:
1048 break;
1049 case O_RCW_FLIPVH:
1051 break;
1052 default:
1054 break;
1055 }
1056 }
1057 }
1058
1059 return v;
1060}
1061
1062JXRHandler::JXRHandler()
1063 : d(new JXRHandlerPrivate)
1064 , m_quality(-1)
1065{
1066}
1067
1068bool JXRHandler::canRead() const
1069{
1070 if (canRead(device())) {
1071 setFormat("jxr");
1072 return true;
1073 }
1074 return false;
1075}
1076
1077bool JXRHandler::canRead(QIODevice *device)
1078{
1079 if (!device) {
1080 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::canRead() called with no device";
1081 return false;
1082 }
1083
1084 // JPEG XR image data is stored in TIFF-like container format (II and 0xBC01 version)
1085 if (device->peek(4) == QByteArray::fromRawData("\x49\x49\xbc\x01", 4)) {
1086 return true;
1087 }
1088
1089 return false;
1090}
1091
1092QImageIOPlugin::Capabilities JXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
1093{
1094 if (format == "jxr") {
1095 return Capabilities(CanRead | CanWrite);
1096 }
1097 if (format == "wdp" || format == "hdp") {
1098 return Capabilities(CanRead);
1099 }
1100 if (!format.isEmpty()) {
1101 return {};
1102 }
1103 if (!device->isOpen()) {
1104 return {};
1105 }
1106
1107 Capabilities cap;
1108 if (device->isReadable() && JXRHandler::canRead(device)) {
1109 cap |= CanRead;
1110 }
1111 if (device->isWritable()) {
1112 cap |= CanWrite;
1113 }
1114 return cap;
1115}
1116
1117QImageIOHandler *JXRPlugin::create(QIODevice *device, const QByteArray &format) const
1118{
1119 QImageIOHandler *handler = new JXRHandler;
1120 handler->setDevice(device);
1121 handler->setFormat(format);
1122 return handler;
1123}
1124
1125#include "moc_jxr_p.cpp"
QFlags< Capability > Capabilities
QVariant read(const QByteArray &data, int versionOverride=0)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QAction * copy(const QObject *recvr, const char *slot, QObject *parent)
QByteArray fromRawData(const char *data, qsizetype size)
bool isEmpty() const const
QColorSpace fromIccProfile(const QByteArray &iccProfile)
QByteArray iccProfile() const const
bool isValid() const const
TransferFunction transferFunction() const const
bool isEmpty() const const
Format format() const const
bool hasAlphaChannel() const const
bool isNull() const const
void setText(const QString &key, const QString &text)
qsizetype sizeInBytes() const const
QString text(const QString &key) const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
int allocationLimit()
virtual void close()
bool isOpen() const const
bool isReadable() const const
bool isWritable() const const
virtual bool open(QIODeviceBase::OpenMode mode)
QByteArray peek(qint64 maxSize)
QByteArray read(qint64 maxSize)
qint64 write(const QByteArray &data)
T * data() const const
bool isNull() const const
QString arg(Args &&... args) const const
QChar * data()
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
bool isNull() const const
QString left(qsizetype n) const const
qsizetype size() const const
QByteArray toUtf8() const const
QUuid createUuid()
QString toString(StringFormat mode) const const
QVariant fromValue(T &&value)
int toInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 3 2025 12:01:07 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.