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_PKPixelFormat32bppRGB;
179 return QImage::Format_RGBX8888; // Format_RGB32 (?)
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 inetger formats.
461 auto qi = source;
462 if (qi.format() == QImage::Format_MonoLSB) {
464 }
465 if (qi.format() == QImage::Format_Indexed8) {
466 if (qi.allGray())
468 else
470 }
471
472 // generic
473 if (!valid.contains(qi.format())) {
474 auto alpha = qi.hasAlphaChannel();
475 auto depth = qi.depth();
476 if (depth >= 12 && depth <= 24 && !alpha) {
477 qi = qi.convertToFormat(QImage::Format_RGB888);
478 } else if (depth >= 48) {
479 // JXR don't have RGBX64 format so I have two possibilities:
480 // - convert to 32 bpp (convertToFormat(alpha ? QImage::Format_RGBA64 : QImage::Format_RGB888))
481 // - convert to 64 bpp with fake alpha (preferred)
482 qi = qi.convertToFormat(QImage::Format_RGBA64);
483 } else {
484 qi = qi.convertToFormat(alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
485 }
486#ifndef JXR_DENY_FLOAT_IMAGE
487 } else if(qi.format() == QImage::Format_RGBA16FPx4 ||
488 qi.format() == QImage::Format_RGBX16FPx4 ||
489 qi.format() == QImage::Format_RGBA32FPx4 ||
491 qi.format() == QImage::Format_RGBX32FPx4) {
492 auto cs = qi.colorSpace();
493 if (cs.isValid() && cs.transferFunction() != QColorSpace::TransferFunction::Linear) {
494 qi = qi.convertedToColorSpace(QColorSpace(QColorSpace::SRgbLinear));
495 }
496 }
497#endif // JXR_DENY_FLOAT_IMAGE
498
499 return qi;
500 }
501
502 /*!
503 * \brief initCodecParameters
504 * Initialize the JXR codec parameters.
505 * \param wmiSCP
506 * \param image The image to save.
507 * \return True on success, otherwise false.
508 */
509 bool initCodecParameters(CWMIStrCodecParam *wmiSCP, const QImage &image)
510 {
511 if (wmiSCP == nullptr || image.isNull()) {
512 return false;
513 }
514 memset(wmiSCP, 0, sizeof(CWMIStrCodecParam));
515
516 auto fmt = image.format();
517
518 wmiSCP->bVerbose = FALSE;
520 wmiSCP->cfColorFormat = Y_ONLY;
521#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
522 else if (fmt == QImage::Format_CMYK8888)
523 wmiSCP->cfColorFormat = CMYK;
524#endif
525 else
526 wmiSCP->cfColorFormat = YUV_444;
527 wmiSCP->bdBitDepth = BD_LONG;
528 wmiSCP->bfBitstreamFormat = FREQUENCY;
529 wmiSCP->bProgressiveMode = TRUE;
530 wmiSCP->olOverlap = OL_ONE;
531 wmiSCP->cNumOfSliceMinus1H = wmiSCP->cNumOfSliceMinus1V = 0;
532 wmiSCP->sbSubband = SB_ALL;
533 wmiSCP->uAlphaMode = image.hasAlphaChannel() ? 2 : 0;
534 return true;
535 }
536
537 /*!
538 * \brief updateTextMetadata
539 * Read the metadata from the image and set it in the encoder.
540 * \param image The image to save.
541 */
542 void updateTextMetadata(const QImage &image)
543 {
544 if (pEncoder == nullptr) {
545 return;
546 }
547
548 DESCRIPTIVEMETADATA meta;
549 memset(&meta, 0, sizeof(meta));
550
551#define META_CTEXT(name, field) \
552 auto field = image.text(QStringLiteral(name)).toUtf8(); \
553 if (!field.isEmpty()) { \
554 meta.field.vt = DPKVT_LPSTR; \
555 meta.field.VT.pszVal = field.data(); \
556 }
557#define META_WTEXT(name, field) \
558 auto field = image.text(QStringLiteral(name)); \
559 if (!field.isEmpty()) { \
560 meta.field.vt = DPKVT_LPWSTR; \
561 meta.field.VT.pwszVal = const_cast<quint16 *>(field.utf16()); \
562 }
563
564 META_CTEXT(META_KEY_DESCRIPTION, pvarImageDescription)
565 META_CTEXT(META_KEY_MANUFACTURER, pvarCameraMake)
566 META_CTEXT(META_KEY_MODEL, pvarCameraModel)
567 META_CTEXT(META_KEY_AUTHOR, pvarArtist)
568 META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright)
569 META_CTEXT(META_KEY_CREATIONDATE, pvarDateTime)
570 META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
571 META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
572 META_WTEXT(META_KEY_TITLE, pvarCaption)
573
574#undef META_CTEXT
575#undef META_WTEXT
576
577 // Software must be updated
578 auto software = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
579 if (!software.isEmpty()) {
580 meta.pvarSoftware.vt = DPKVT_LPSTR;
581 meta.pvarSoftware.VT.pszVal = software.data();
582 }
583
584 auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
585 if (!xmp.isNull()) {
586 if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<quint8 *>(xmp.data()), xmp.size())) {
587 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err;
588 }
589 }
590 if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) {
591 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err;
592 }
593 }
594
595 /*!
596 * \brief exactFormat
597 * JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
598 * \param jxrFormat Format to be converted.
599 * \return A valid Qt format or QImage::Format_Invalid if there is no match
600 */
601 static QImage::Format exactFormat(const PKPixelFormatGUID &jxrFormat)
602 {
603 auto l = exactMatchingFormats();
604 for (auto &&p : l) {
605 if (IsEqualGUID(p.second, jxrFormat))
606 return p.first;
607 }
609 }
610
611 /*!
612 * \brief exactFormat
613 * JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
614 * \param qtFormat Format to be converted.
615 * \return A valid JXR format or GUID_PKPixelFormatUndefined if there is no match
616 */
617 static PKPixelFormatGUID exactFormat(const QImage::Format &qtFormat)
618 {
619 auto l = exactMatchingFormats();
620 for (auto &&p : l) {
621 if (p.first == qtFormat)
622 return p.second;
623 }
624 return GUID_PKPixelFormatUndefined;
625 }
626
627private:
628 static QList<std::pair<QImage::Format, PKPixelFormatGUID>> exactMatchingFormats()
629 {
630 // clang-format off
632#ifndef JXR_DENY_FLOAT_IMAGE
633 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA16FPx4, GUID_PKPixelFormat64bppRGBAHalf)
634 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX16FPx4, GUID_PKPixelFormat64bppRGBHalf)
635 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4, GUID_PKPixelFormat128bppRGBAFloat)
636 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4_Premultiplied, GUID_PKPixelFormat128bppPRGBAFloat)
637 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX32FPx4, GUID_PKPixelFormat128bppRGBFloat)
638#endif // JXR_DENY_FLOAT_IMAGE
639#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
640 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYK)
641 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYKDIRECT)
642#endif
643 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Mono, GUID_PKPixelFormatBlackWhite)
644 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale8, GUID_PKPixelFormat8bppGray)
645 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale16, GUID_PKPixelFormat16bppGray)
646 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB555, GUID_PKPixelFormat16bppRGB565)
647 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB16, GUID_PKPixelFormat16bppRGB565)
648 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_BGR888, GUID_PKPixelFormat24bppBGR)
649 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB888, GUID_PKPixelFormat24bppRGB)
650 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX8888, GUID_PKPixelFormat32bppRGB)
651 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888, GUID_PKPixelFormat32bppRGBA)
652 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888_Premultiplied, GUID_PKPixelFormat32bppPRGBA)
653 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64, GUID_PKPixelFormat64bppRGBA)
654 << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64_Premultiplied, GUID_PKPixelFormat64bppPRGBA);
655 // clang-format on
656 return list;
657 }
658
659 bool deviceCopy(QIODevice *target, QIODevice *source)
660 {
661 if (target == nullptr || source == nullptr) {
662 return false;
663 }
664 auto isTargetOpen = target->isOpen();
665 if (!isTargetOpen && !target->open(QIODevice::WriteOnly)) {
666 return false;
667 }
668 auto isSourceOpen = source->isOpen();
669 if (!isSourceOpen && !source->open(QIODevice::ReadOnly)) {
670 return false;
671 }
672 QByteArray buff(32768 * 4, char());
673 for (; !source->atEnd();) {
674 auto read = source->read(buff.data(), buff.size());
675 if (read < 1) {
676 return false;
677 }
678 if (target->write(buff.data(), read) != read) {
679 return false;
680 }
681 }
682 if (!isSourceOpen) {
683 source->close();
684 }
685 if (!isTargetOpen) {
686 target->close();
687 }
688 return true;
689 }
690
691 bool readDevice(QIODevice *device)
692 {
693 if (device == nullptr) {
694 return false;
695 }
696 if (!jxrFile.isNull()) {
697 return true;
698 }
699 // I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
700 auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
701 QSharedPointer<QFile> file(new QFile(fileName));
702 if (!file->open(QFile::WriteOnly)) {
703 return false;
704 }
705 if (!deviceCopy(file.data(), device)) {
706 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::readDevice() error while writing in the target device";
707 return false;
708 }
709 file->close();
710 jxrFile = file;
711 return true;
712 }
713
714 bool initDecoder()
715 {
716 if (pDecoder) {
717 return true;
718 }
719 if (pCodecFactory == nullptr) {
720 return false;
721 }
722 if (auto err = pCodecFactory->CreateDecoderFromFile(qUtf8Printable(fileName()), &pDecoder)) {
723 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initDecoder() unable to create decoder:" << err;
724 return false;
725 }
726 return true;
727 }
728
729 bool initEncoder()
730 {
731 if (pDecoder) {
732 return true;
733 }
734 if (pCodecFactory == nullptr) {
735 return false;
736 }
737 if (auto err = pCodecFactory->CreateCodec(&IID_PKImageWmpEncode, (void **)&pEncoder)) {
738 qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initEncoder() unable to create encoder:" << err;
739 return false;
740 }
741 return true;
742 }
743
744 bool readTextMeta() const {
745 if (pDecoder == nullptr) {
746 return false;
747 }
748 if (!txtMeta.isEmpty()) {
749 return true;
750 }
751
752 DESCRIPTIVEMETADATA meta;
753 if (pDecoder->GetDescriptiveMetadata(pDecoder, &meta)) {
754 return false;
755 }
756
757#define META_TEXT(name, field) \
758 if (meta.field.vt == DPKVT_LPSTR) \
759 txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \
760 else if (meta.field.vt == DPKVT_LPWSTR) \
761 txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));
762
763 META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription)
764 META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake)
765 META_TEXT(META_KEY_MODEL, pvarCameraModel)
766 META_TEXT(META_KEY_SOFTWARE, pvarSoftware)
767 META_TEXT(META_KEY_CREATIONDATE, pvarDateTime)
768 META_TEXT(META_KEY_AUTHOR, pvarArtist)
769 META_TEXT(META_KEY_COPYRIGHT, pvarCopyright)
770 META_TEXT(META_KEY_TITLE, pvarCaption)
771 META_TEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
772 META_TEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
773
774#undef META_TEXT
775
776 return true;
777 }
778};
779
780bool JXRHandler::read(QImage *outImage)
781{
782 if (!d->initForReading(device())) {
783 return false;
784 }
785
786 PKPixelFormatGUID convFmt;
787 auto imageFmt = d->imageFormat(&convFmt);
788 auto img = imageAlloc(d->imageSize(), imageFmt);
789 if (img.isNull()) {
790 return false;
791 }
792
793 // resolution
794 float hres, vres;
795 if (auto err = d->pDecoder->GetResolution(d->pDecoder, &hres, &vres)) {
796 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while reading resolution:" << err;
797 } else {
798 img.setDotsPerMeterX(qRound(hres * 1000 / 25.4));
799 img.setDotsPerMeterY(qRound(vres * 1000 / 25.4));
800 }
801
802 // alpha copy mode
803 if (img.hasAlphaChannel()) {
804 d->pDecoder->WMP.wmiSCP.uAlphaMode = 2; // or 1 (?)
805 }
806
807 PKRect rect = {0, 0, img.width(), img.height()};
808 if (IsEqualGUID(convFmt, GUID_PKPixelFormatUndefined)) { // direct storing
809 if (auto err = d->pDecoder->Copy(d->pDecoder, &rect, img.bits(), img.bytesPerLine())) {
810 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy data:" << err;
811 return false;
812 }
813 } else { // conversion to a known format
814 PKFormatConverter *pConverter = nullptr;
815 if (auto err = d->pCodecFactory->CreateFormatConverter(&pConverter)) {
816 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to create the converter:" << err;
817 return false;
818 }
819 if (auto err = pConverter->Initialize(pConverter, d->pDecoder, nullptr, convFmt)) {
820 PKFormatConverter_Release(&pConverter);
821 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to initialize the converter:" << err;
822 return false;
823 }
824 if (d->pDecoder->WMP.wmiI.cBitsPerUnit == size_t(img.depth())) { // in place conversion
825 if (auto err = pConverter->Copy(pConverter, &rect, img.bits(), img.bytesPerLine())) {
826 PKFormatConverter_Release(&pConverter);
827 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
828 return false;
829 }
830 } else { // additional buffer needed
831 qint64 convStrideSize = (img.width() * d->pDecoder->WMP.wmiI.cBitsPerUnit + 7) / 8;
832 qint64 buffSize = convStrideSize * img.height();
833 qint64 limit = QImageReader::allocationLimit();
834 if (limit && (buffSize + img.sizeInBytes()) > limit * 1024 * 1024) {
835 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to covert due to allocation limit set:" << limit << "MiB";
836 return false;
837 }
838 QVector<quint8> ba(buffSize);
839 if (auto err = pConverter->Copy(pConverter, &rect, ba.data(), convStrideSize)) {
840 PKFormatConverter_Release(&pConverter);
841 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
842 return false;
843 }
844 for (qint32 y = 0, h = img.height(); y < h; ++y) {
845 std::memcpy(img.scanLine(y), ba.data() + convStrideSize * y, (std::min)(convStrideSize, img.bytesPerLine()));
846 }
847 }
848 PKFormatConverter_Release(&pConverter);
849 }
850
851 // Metadata (e.g.: icc profile, description, etc...)
852 img.setColorSpace(d->colorSpace());
853 d->setTextMetadata(img);
854
855#ifndef JXR_DENY_FLOAT_IMAGE
856 // JXR float are stored in scRGB.
857 if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied ||
858 img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4 || img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
859 auto hasAlpha = img.hasAlphaChannel();
860 for (qint32 y = 0, h = img.height(); y < h; ++y) {
861 if (img.depth() == 64) {
862 auto line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
863 for (int x = 0, w = img.width() * 4; x < w; x += 4)
864 line[x + 3] = hasAlpha ? std::clamp(line[x + 3], qfloat16(0), qfloat16(1)) : qfloat16(1);
865 } else {
866 auto line = reinterpret_cast<float *>(img.scanLine(y));
867 for (int x = 0, w = img.width() * 4; x < w; x += 4)
868 line[x + 3] = hasAlpha ? std::clamp(line[x + 3], float(0), float(1)) : float(1);
869 }
870 }
871 if(!img.colorSpace().isValid()) {
872 img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
873 }
874 }
875#endif
876
877 *outImage = img;
878 return true;
879}
880
881bool JXRHandler::write(const QImage &image)
882{
883 if (!d->initForWriting()) {
884 return false;
885 }
886 struct WMPStream *pEncodeStream = nullptr;
887 if (auto err = d->pFactory->CreateStreamFromFilename(&pEncodeStream, qUtf8Printable(d->fileName()), "wb")) {
888 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() unable to create stream:" << err;
889 return false;
890 }
891
892 // convert the image to a supported format
893 auto qi = d->imageToSave(image);
894 auto jxlfmt = d->exactFormat(qi.format());
895 if (IsEqualGUID(jxlfmt, GUID_PKPixelFormatUndefined)) {
896 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating the target format for" << qi.format();
897 return false;
898 }
899#ifndef JXR_DISABLE_BGRA_HACK
900 if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGBA)) {
901 jxlfmt = GUID_PKPixelFormat32bppBGRA;
902 qi.rgbSwap();
903 }
904 if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppPRGBA)) {
905 jxlfmt = GUID_PKPixelFormat32bppPBGRA;
906 qi.rgbSwap();
907 }
908#endif
909
910 // initialize the codec parameters
911 CWMIStrCodecParam wmiSCP;
912 if (!d->initCodecParameters(&wmiSCP, qi)) {
913 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating encoder parameters for" << qi.format();
914 return false;
915 }
916 if (m_quality > -1) {
917 wmiSCP.uiDefaultQPIndex = qBound(0, 100 - m_quality, 100);
918 }
919
920 if (auto err = d->pEncoder->Initialize(d->pEncoder, pEncodeStream, &wmiSCP, sizeof(wmiSCP))) {
921 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while initializing the encoder:" << err;
922 return false;
923 }
924
925 // setting mandatory image info
926 if (auto err = d->pEncoder->SetPixelFormat(d->pEncoder, jxlfmt)) {
927 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image format:" << err;
928 return false;
929 }
930 if (auto err = d->pEncoder->SetSize(d->pEncoder, qi.width(), qi.height())) {
931 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image size:" << err;
932 return false;
933 }
934 if (auto err = d->pEncoder->SetResolution(d->pEncoder, qi.dotsPerMeterX() * 25.4 / 1000, qi.dotsPerMeterY() * 25.4 / 1000)) {
935 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image resolution:" << err;
936 return false;
937 }
938
939 // setting metadata (a failure of setting metadata doesn't stop the encoding)
940 auto cs = qi.colorSpace().iccProfile();
941 if (!cs.isEmpty()) {
942 if (auto err = d->pEncoder->SetColorContext(d->pEncoder, reinterpret_cast<quint8 *>(cs.data()), cs.size())) {
943 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting ICC profile:" << err;
944 }
945 }
946 d->updateTextMetadata(image);
947
948 // writing the image
949 if (auto err = d->pEncoder->WritePixels(d->pEncoder, qi.height(), qi.bits(), qi.bytesPerLine())) {
950 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while encoding the image:" << err;
951 return false;
952 }
953 if (!d->finalizeWriting(device())) {
954 return false;
955 }
956 return true;
957}
958
959void JXRHandler::setOption(ImageOption option, const QVariant &value)
960{
961 if (option == QImageIOHandler::Quality) {
962 bool ok = false;
963 auto q = value.toInt(&ok);
964 if (ok) {
965 m_quality = q;
966 }
967 }
968}
969
970bool JXRHandler::supportsOption(ImageOption option) const
971{
972 if (option == QImageIOHandler::Size) {
973 return true;
974 }
975 if (option == QImageIOHandler::ImageFormat) {
976 return true;
977 }
978 if (option == QImageIOHandler::Quality) {
979 return true;
980 }
982 return false; // disabled because test cases are missing
983 }
984 return false;
985}
986
987QVariant JXRHandler::option(ImageOption option) const
988{
989 QVariant v;
990
991 if (option == QImageIOHandler::Size) {
992 if (d->initForReading(device())) {
993 auto size = d->imageSize();
994 if (size.isValid()) {
995 v = QVariant::fromValue(size);
996 }
997 }
998 }
999
1000 if (option == QImageIOHandler::ImageFormat) {
1001 if (d->initForReading(device())) {
1002 v = QVariant::fromValue(d->imageFormat());
1003 }
1004 }
1005
1006 if (option == QImageIOHandler::Quality) {
1007 v = m_quality;
1008 }
1009
1011 // TODO: rotation info (test case needed)
1012 if (d->initForReading(device())) {
1013 switch (d->pDecoder->WMP.oOrientationFromContainer) {
1014 case O_FLIPV:
1016 break;
1017 case O_FLIPH:
1019 break;
1020 case O_FLIPVH:
1022 break;
1023 case O_RCW:
1025 break;
1026 case O_RCW_FLIPV:
1028 break;
1029 case O_RCW_FLIPH:
1031 break;
1032 case O_RCW_FLIPVH:
1034 break;
1035 default:
1037 break;
1038 }
1039 }
1040 }
1041
1042 return v;
1043}
1044
1045JXRHandler::JXRHandler()
1046 : d(new JXRHandlerPrivate)
1047 , m_quality(-1)
1048{
1049}
1050
1051bool JXRHandler::canRead() const
1052{
1053 if (canRead(device())) {
1054 setFormat("jxr");
1055 return true;
1056 }
1057 return false;
1058}
1059
1060bool JXRHandler::canRead(QIODevice *device)
1061{
1062 if (!device) {
1063 qCWarning(LOG_JXRPLUGIN) << "JXRHandler::canRead() called with no device";
1064 return false;
1065 }
1066
1067 // Some tests on sequential devices fail: I reject them for now
1068 if (device->isSequential()) {
1069 return false;
1070 }
1071
1072 // JPEG XR image data is stored in TIFF-like container format (II and 0xBC01 version)
1073 if (device->peek(4) == QByteArray::fromRawData("\x49\x49\xbc\x01", 4)) {
1074 return true;
1075 }
1076
1077 return false;
1078}
1079
1080QImageIOPlugin::Capabilities JXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
1081{
1082 if (format == "jxr" || format == "wdp" || format == "hdp") {
1083 return Capabilities(CanRead | CanWrite);
1084 }
1085 if (!format.isEmpty()) {
1086 return {};
1087 }
1088 if (!device->isOpen()) {
1089 return {};
1090 }
1091
1092 Capabilities cap;
1093 if (device->isReadable() && JXRHandler::canRead(device)) {
1094 cap |= CanRead;
1095 }
1096 if (device->isWritable()) {
1097 cap |= CanWrite;
1098 }
1099 return cap;
1100}
1101
1102QImageIOHandler *JXRPlugin::create(QIODevice *device, const QByteArray &format) const
1103{
1104 QImageIOHandler *handler = new JXRHandler;
1105 handler->setDevice(device);
1106 handler->setFormat(format);
1107 return handler;
1108}
1109
1110#include "moc_jxr_p.cpp"
QFlags< Capability > Capabilities
KIOCORE_EXPORT CopyJob * copy(const QList< QUrl > &src, const QUrl &dest, JobFlags flags=DefaultFlags)
QVariant read(const QByteArray &data, int versionOverride=0)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
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
QImage convertToFormat(Format format, Qt::ImageConversionFlags flags) &&
Format format() const const
bool hasAlphaChannel() const const
bool isNull() const const
void setText(const QString &key, const QString &text)
QString text(const QString &key) const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
typedef Capabilities
int allocationLimit()
virtual bool atEnd() const const
virtual void close()
bool isOpen() const const
bool isReadable() const const
virtual bool isSequential() 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-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:59:35 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.