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

KDE's Doxygen guidelines are available online.