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

KDE's Doxygen guidelines are available online.