Krita Source Code Documentation
Loading...
Searching...
No Matches
JPEGXLImport.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2021 the JPEG XL Project Authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *
5 * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy@amyspark.me>
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9#include "JPEGXLImport.h"
10#include <KoColorProfileQuery.h>
11
13
14#include <jxl/decode_cxx.h>
15#include <jxl/resizable_parallel_runner_cxx.h>
16#include <jxl/types.h>
17#include <kpluginfactory.h>
18
19#include <QBuffer>
20#include <algorithm>
21#include <array>
22#include <cstring>
23#include <map>
24
25#include <KisDocument.h>
28#include <KoColorProfile.h>
31#include <KoConfig.h>
33#include <filter/kis_filter.h>
36#include <kis_assert.h>
37#include <kis_debug.h>
38#include <kis_group_layer.h>
40#include <kis_iterator_ng.h>
42#include <kis_paint_layer.h>
44
45K_PLUGIN_FACTORY_WITH_JSON(ImportFactory, "krita_jxl_import.json", registerPlugin<JPEGXLImport>();)
46
47static constexpr std::array<char, 4> exifTag = {'e', 'x', 'i', 'f'};
48static constexpr std::array<char, 4> xmpTag = {'x', 'm', 'l', ' '};
49
50class Q_DECL_HIDDEN JPEGXLImportData
51{
52public:
53 JxlBasicInfo m_info{};
54 JxlExtraChannelInfo m_extra{};
55 JxlPixelFormat m_pixelFormat{};
56 JxlPixelFormat m_pixelFormat_target{};
57 JxlFrameHeader m_header{};
58 std::vector<quint8> m_rawData{};
59 KisPaintDeviceSP m_currentFrame{nullptr};
60 uint32_t cmykChannelID = 0;
61 int m_nextFrameTime{0};
62 int m_durationFrameInTicks{0};
68 bool isCMYK = false;
69 bool applyOOTF = true;
70 float displayGamma = 1.2f;
71 float displayNits = 1000.0;
73 const KoColorSpace *cs = nullptr;
74 const KoColorSpace *cs_target = nullptr;
75 const KoColorSpace *cs_intermediate = nullptr;
76 std::vector<quint8> kPlane;
78};
79
80template<LinearizePolicy policy>
81inline float linearizeValueAsNeeded(float value)
82{
83 if (policy == LinearizePolicy::LinearFromPQ) {
85 } else if (policy == LinearizePolicy::LinearFromHLG) {
86 return removeHLGCurve(value);
87 } else if (policy == LinearizePolicy::LinearFromSMPTE428) {
89 }
90 return value;
91}
92
93template<LinearizePolicy policy, typename T, typename std::enable_if_t<std::numeric_limits<T>::is_integer, int> = 1>
94inline float value(const T *src, size_t ch)
95{
96 float v = float(src[ch]) / float(std::numeric_limits<T>::max());
97
98 return linearizeValueAsNeeded<policy>(v);
99}
100
101template<LinearizePolicy policy, typename T, typename std::enable_if_t<!std::numeric_limits<T>::is_integer, int> = 1>
102inline float value(const T *src, size_t ch)
103{
104 float v = float(src[ch]);
105
106 return linearizeValueAsNeeded<policy>(v);
107}
108
109template<typename channelsType, bool swap, LinearizePolicy policy, bool applyOOTF>
111{
112 const uint32_t xPos = d.m_header.layer_info.crop_x0;
113 const uint32_t yPos = d.m_header.layer_info.crop_y0;
114 const uint32_t width = d.m_header.layer_info.xsize;
115 const uint32_t height = d.m_header.layer_info.ysize;
116 KisHLineIteratorSP it = d.m_currentFrame->createHLineIteratorNG(xPos, yPos, width);
117
118 const auto *src = reinterpret_cast<const channelsType *>(d.m_rawData.data());
119 const uint32_t channels = d.m_pixelFormat.num_channels;
120
121 if (policy != LinearizePolicy::KeepTheSame) {
122 const KoColorSpace *cs = d.cs;
123 const double *lCoef = d.lCoef.constData();
124 QVector<float> pixelValues(static_cast<int>(cs->channelCount()));
125 float *tmp = pixelValues.data();
126 const quint32 alphaPos = cs->alphaPos();
127
128 for (size_t j = 0; j < height; j++) {
129 for (size_t i = 0; i < width; i++) {
130 for (size_t i = 0; i < channels; i++) {
131 tmp[i] = 1.0;
132 }
133
134 for (size_t ch = 0; ch < channels; ch++) {
135 if (ch == alphaPos) {
136 tmp[ch] = value<LinearizePolicy::KeepTheSame, channelsType>(src, ch);
137 } else {
138 tmp[ch] = value<policy, channelsType>(src, ch);
139 }
140 }
141
142 if (swap) {
143 std::swap(tmp[0], tmp[2]);
144 }
145
146 if (policy == LinearizePolicy::LinearFromHLG && applyOOTF) {
147 applyHLGOOTF(tmp, lCoef, d.displayGamma, d.displayNits);
148 }
149
150 cs->fromNormalisedChannelsValue(it->rawData(), pixelValues);
151
152 src += d.m_pixelFormat.num_channels;
153
154 it->nextPixel();
155 }
156 it->nextRow();
157 }
158 } else {
159 for (size_t j = 0; j < height; j++) {
160 for (size_t i = 0; i < width; i++) {
161 auto *dst = reinterpret_cast<channelsType *>(it->rawData());
162
163 std::memcpy(dst, src, channels * sizeof(channelsType));
164
165 if (swap) {
166 std::swap(dst[0], dst[2]);
167 } else if (d.isCMYK && d.m_info.uses_original_profile) {
168 // Swap alpha and key channel for CMYK
169 std::swap(dst[3], dst[4]);
170 }
171
172 src += d.m_pixelFormat.num_channels;
173
174 it->nextPixel();
175 }
176 it->nextRow();
177 }
178 }
179}
180
181template<typename channelsType, bool swap, LinearizePolicy policy>
183{
184 if (d.applyOOTF) {
185 imageOutCallback<channelsType, swap, policy, true>(d);
186 } else {
187 imageOutCallback<channelsType, swap, policy, false>(d);
188 }
189}
190
191template<typename channelsType, bool swap>
193{
194 switch (d.linearizePolicy) {
196 generateCallbackWithPolicy<channelsType, swap, LinearizePolicy::LinearFromPQ>(d);
197 break;
199 generateCallbackWithPolicy<channelsType, swap, LinearizePolicy::LinearFromHLG>(d);
200 break;
202 generateCallbackWithPolicy<channelsType, swap, LinearizePolicy::LinearFromSMPTE428>(d);
203 break;
205 default:
206 generateCallbackWithPolicy<channelsType, swap, LinearizePolicy::KeepTheSame>(d);
207 break;
208 };
209}
210
211template<typename channelsType>
213{
214 if (d.m_colorID == RGBAColorModelID
215 && (d.m_depthID == Integer8BitsColorDepthID || d.m_depthID == Integer16BitsColorDepthID)
216 && d.linearizePolicy == LinearizePolicy::KeepTheSame) {
217 generateCallbackWithSwap<channelsType, true>(d);
218 } else {
219 generateCallbackWithSwap<channelsType, false>(d);
220 }
221}
222
224{
225 switch (d.m_pixelFormat.data_type) {
226 case JXL_TYPE_FLOAT:
227 return generateCallbackWithType<float>(d);
228 case JXL_TYPE_UINT8:
229 return generateCallbackWithType<uint8_t>(d);
230 case JXL_TYPE_UINT16:
231 return generateCallbackWithType<uint16_t>(d);
232#ifdef HAVE_OPENEXR
233 case JXL_TYPE_FLOAT16:
234 return generateCallbackWithType<half>(d);
235 break;
236#endif
237 default:
238 KIS_ASSERT_X(false, "JPEGXL::generateCallback", "Unknown image format!");
239 }
240}
241
242JPEGXLImport::JPEGXLImport(QObject *parent, const QVariantList &)
243 : KisImportExportFilter(parent)
244{
245}
246
248JPEGXLImport::convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP /*configuration*/)
249{
250 if (!io->isReadable()) {
251 errFile << "Cannot read image contents";
253 }
254
256 const auto data = io->readAll();
257
258 const auto validation =
259 JxlSignatureCheck(reinterpret_cast<const uint8_t *>(data.constData()), static_cast<size_t>(data.size()));
260
261 switch (validation) {
262 case JXL_SIG_NOT_ENOUGH_BYTES:
263 errFile << "Failed magic byte validation, not enough data";
265 case JXL_SIG_INVALID:
266 errFile << "Failed magic byte validation, incorrect format";
268 default:
269 break;
270 }
271
272 // Multi-threaded parallel runner.
273 auto runner = JxlResizableParallelRunnerMake(nullptr);
274 auto dec = JxlDecoderMake(nullptr);
275
277
278 // List of blend mode that we can currently support
279 static constexpr std::array<JxlBlendMode, 3> supportedBlendMode = {JXL_BLEND_REPLACE, JXL_BLEND_BLEND, JXL_BLEND_MULADD};
280
281 // Metadata and frame header decoding
282 bool isAnimated = false;
283 bool isMultilayer = false;
284 bool isMultipage = false;
285 bool forceCoalesce = false;
286 {
287 if (JXL_DEC_SUCCESS
288 != JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FRAME)) {
289 errFile << "JxlDecoderSubscribeEvents failed";
291 }
292
293 if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner.get())) {
294 errFile << "JxlDecoderSetParallelRunner failed";
296 }
297
298 if (JXL_DEC_SUCCESS
299 != JxlDecoderSetInput(dec.get(),
300 reinterpret_cast<const uint8_t *>(data.constData()),
301 static_cast<size_t>(data.size()))) {
302 errFile << "JxlDecoderSetInput failed";
304 };
305 JxlDecoderCloseInput(dec.get());
306
307 if (JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec.get(), JXL_TRUE)) {
308 errFile << "JxlDecoderSetDecompressBoxes failed";
310 };
311
312 if (JXL_DEC_SUCCESS != JxlDecoderSetCoalescing(dec.get(), JXL_FALSE)) {
313 errFile << "JxlDecoderSetCoalescing failed";
315 };
316
317 for (;;) {
318 JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
319
320 if (status == JXL_DEC_ERROR) {
321 errFile << "Decoder error";
323 } else if (status == JXL_DEC_NEED_MORE_INPUT) {
324 errFile << "Error, already provided all input";
326 } else if (status == JXL_DEC_BASIC_INFO) {
327 if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &d.m_info)) {
328 errFile << "JxlDecoderGetBasicInfo failed";
330 }
331 // Coalesce frame on animation import
332 if (d.m_info.have_animation) {
333 isMultilayer = false;
334 isAnimated = true;
335 isMultipage = true;
336 forceCoalesce = true;
337 }
338
339 dbgFile << "Extra Channel[s] info:";
340 for (uint32_t i = 0; i < d.m_info.num_extra_channels; i++) {
341 if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec.get(), i, &d.m_extra)) {
342 errFile << "JxlDecoderGetExtraChannelInfo failed";
343 break;
344 }
345 // Channel name references taken from libjxl repo:
346 // https://github.com/libjxl/libjxl/blob/v0.8.0/lib/extras/enc/pnm.cc#L262
347 // With added "JXL-" prefix to indicate that it comes from JXL image.
348 const QString channelTypeString = [&]() {
349 switch (d.m_extra.type) {
350 case JXL_CHANNEL_ALPHA:
351 return QString("JXL-Alpha");
352 case JXL_CHANNEL_DEPTH:
353 return QString("JXL-Depth");
354 case JXL_CHANNEL_SPOT_COLOR:
355 return QString("JXL-SpotColor");
356 case JXL_CHANNEL_SELECTION_MASK:
357 return QString("JXL-SelectionMask");
358 case JXL_CHANNEL_BLACK:
359 return QString("JXL-Black");
360 case JXL_CHANNEL_CFA:
361 return QString("JXL-CFA");
362 case JXL_CHANNEL_THERMAL:
363 return QString("JXL-Thermal");
364 default:
365 return QString("JXL-UNKNOWN");
366 }
367 }();
368
369 // List all extra channels
370 dbgFile << "index:" << i << " | type:" << channelTypeString;
371 if (d.m_extra.type == JXL_CHANNEL_BLACK) {
372 d.isCMYK = true;
373 d.cmykChannelID = i;
374 }
375 if (d.m_extra.type == JXL_CHANNEL_SPOT_COLOR) {
376 warnFile << "Spot color channels unsupported! Rewinding decoder with coalescing enabled";
377 document->setWarningMessage(i18nc("JPEG-XL errors",
378 "Detected JPEG-XL image with spot color channels, "
379 "importing flattened image."));
380 forceCoalesce = true;
381 }
382 }
383
384 dbgFile << "Info";
385 dbgFile << "Size:" << d.m_info.xsize << "x" << d.m_info.ysize;
386 dbgFile << "Depth:" << d.m_info.bits_per_sample << d.m_info.exponent_bits_per_sample;
387 dbgFile << "Number of color channels:" << d.m_info.num_color_channels;
388 dbgFile << "Number of extra channels:" << d.m_info.num_extra_channels;
389 dbgFile << "Extra channels depth:" << d.m_info.alpha_bits << d.m_info.alpha_exponent_bits;
390 dbgFile << "Has animation:" << d.m_info.have_animation << "loops:" << d.m_info.animation.num_loops
391 << "tick:" << d.m_info.animation.tps_numerator << d.m_info.animation.tps_denominator;
392 dbgFile << "Internal pixel format:" << (d.m_info.uses_original_profile ? "Original" : "XYB");
393 JxlResizableParallelRunnerSetThreads(
394 runner.get(),
395 JxlResizableParallelRunnerSuggestThreads(d.m_info.xsize, d.m_info.ysize));
396
397 if (d.m_info.exponent_bits_per_sample != 0) {
398 if (d.m_info.bits_per_sample <= 16) {
399 d.m_pixelFormat.data_type = JXL_TYPE_FLOAT16;
400 d.m_depthID = Float16BitsColorDepthID;
401 } else if (d.m_info.bits_per_sample <= 32) {
402 d.m_pixelFormat.data_type = JXL_TYPE_FLOAT;
403 d.m_depthID = Float32BitsColorDepthID;
404 } else {
405 errFile << "Unsupported JPEG-XL input depth" << d.m_info.bits_per_sample
406 << d.m_info.exponent_bits_per_sample;
408 }
409 } else if (d.m_info.bits_per_sample <= 8) {
410 d.m_pixelFormat.data_type = JXL_TYPE_UINT8;
411 d.m_depthID = Integer8BitsColorDepthID;
412 } else if (d.m_info.bits_per_sample <= 16) {
413 d.m_pixelFormat.data_type = JXL_TYPE_UINT16;
414 d.m_depthID = Integer16BitsColorDepthID;
415 } else {
416 errFile << "Unsupported JPEG-XL input depth" << d.m_info.bits_per_sample
417 << d.m_info.exponent_bits_per_sample;
419 }
420
421 if (d.m_info.num_color_channels == 1) {
422 // Grayscale
423 d.m_pixelFormat.num_channels = 2;
424 d.m_colorID = GrayAColorModelID;
425 } else if (d.m_info.num_color_channels == 3 && !d.isCMYK) {
426 // RGBA
427 d.m_pixelFormat.num_channels = 4;
428 d.m_colorID = RGBAColorModelID;
429 } else if (d.m_info.num_color_channels == 3 && d.isCMYK) {
430 // CMYKA
431 d.m_pixelFormat.num_channels = 4;
432 d.m_colorID = CMYKAColorModelID;
433 } else {
434 warnFile << "Forcing a RGBA conversion, unknown color space";
435 d.m_pixelFormat.num_channels = 4;
436 d.m_colorID = RGBAColorModelID;
437 }
438
439 if (!d.m_info.uses_original_profile) {
440 d.m_pixelFormat_target.data_type = d.m_pixelFormat.data_type;
441 d.m_pixelFormat.data_type = JXL_TYPE_FLOAT;
442 d.m_depthID_target = d.m_depthID;
443 d.m_colorID_target = d.m_colorID;
444 d.m_depthID = Float32BitsColorDepthID;
445
446 if (d.m_colorID != GrayAColorModelID) {
447 d.m_colorID = RGBAColorModelID;
448 }
449 }
450 } else if (status == JXL_DEC_FRAME) {
451 if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec.get(), &d.m_header)) {
452 errFile << "JxlDecoderGetFrameHeader failed";
454 }
455
456 const JxlBlendMode blendMode = d.m_header.layer_info.blend_info.blendmode;
457 const bool isBlendSupported =
458 std::find(supportedBlendMode.begin(), supportedBlendMode.end(), blendMode) != supportedBlendMode.end();
459
460 if (!isBlendSupported) {
461 forceCoalesce = true;
462 }
463
464 if (d.m_header.duration == 0) {
465 isMultilayer = true;
466 } else if (d.m_header.duration == 0xFFFFFFFF) {
467 isMultipage = true;
468 isAnimated = false;
469 forceCoalesce = true;
470 } else {
471 isAnimated = true;
472 isMultipage = false;
473 isMultilayer = false;
474 forceCoalesce = true;
475 }
476 } else if (status == JXL_DEC_SUCCESS) {
477 break;
478 }
479 }
480 }
481 JxlDecoderReset(dec.get());
482
483 // Set coalescing FALSE to enable layered JXL
484 if (JXL_DEC_SUCCESS != JxlDecoderSetCoalescing(dec.get(), forceCoalesce ? JXL_TRUE : JXL_FALSE)) {
485 errFile << "JxlDecoderSetCoalescing failed";
487 }
488
489 if (JXL_DEC_SUCCESS
490 != JxlDecoderSubscribeEvents(dec.get(),
491 JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE | JXL_DEC_BOX | JXL_DEC_FRAME)) {
492 errFile << "JxlDecoderSubscribeEvents failed";
494 }
495
496 if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner.get())) {
497 errFile << "JxlDecoderSetParallelRunner failed";
499 }
500
501 if (JXL_DEC_SUCCESS
502 != JxlDecoderSetInput(dec.get(),
503 reinterpret_cast<const uint8_t *>(data.constData()),
504 static_cast<size_t>(data.size()))) {
505 errFile << "JxlDecoderSetInput failed";
507 };
508 JxlDecoderCloseInput(dec.get());
509
510 if (JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec.get(), JXL_TRUE)) {
511 errFile << "JxlDecoderSetDecompressBoxes failed";
513 };
514
515 KisImageSP image{nullptr};
516 KisLayerSP layer{nullptr};
517 std::multimap<QByteArray, QByteArray> metadataBoxes;
518 std::vector<KisLayerSP> additionalLayers;
519 bool bgLayerSet = false;
520 bool needColorTransform = false;
521 bool needIntermediateTransform = false;
522 QByteArray boxType(5, 0x0);
523 QByteArray box(16384, 0x0);
524 auto boxSize = box.size();
525
526 // Basic info already parsed above, skip doing it again
527 for (;;) {
528 JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
529
530 if (status == JXL_DEC_ERROR) {
531 errFile << "Decoder error";
533 } else if (status == JXL_DEC_NEED_MORE_INPUT) {
534 errFile << "Error, already provided all input";
536 } else if (status == JXL_DEC_COLOR_ENCODING) {
537 // Determine color space information
538 const KoColorProfile *profile = nullptr;
539 const KoColorProfile *profileTarget = nullptr;
540 const KoColorProfile *profileIntermediate = nullptr;
541
542 // Chrome way of decoding JXL implies first scanning
543 // the CICP encoding for HDR, and only afterwards
544 // falling back to ICC.
545 JxlColorEncoding colorEncoding{};
546 if (JXL_DEC_SUCCESS
547 == JxlDecoderGetColorAsEncodedProfile(dec.get(),
548#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
549 nullptr,
550#endif
551 JXL_COLOR_PROFILE_TARGET_DATA,
552 &colorEncoding)) {
553 const TransferCharacteristics transferFunction = [&]() {
554 switch (colorEncoding.transfer_function) {
555 case JXL_TRANSFER_FUNCTION_PQ: {
556 dbgFile << "linearizing from PQ";
557 d.linearizePolicy = LinearizePolicy::LinearFromPQ;
558 return TRC_LINEAR;
559 }
560 case JXL_TRANSFER_FUNCTION_HLG: {
561 dbgFile << "linearizing from HLG";
562 if (!document->fileBatchMode()) {
563 KisDlgHLGImport dlg(d.applyOOTF, d.displayGamma, d.displayNits);
564 dlg.exec();
565 d.applyOOTF = dlg.applyOOTF();
566 d.displayGamma = dlg.gamma();
567 d.displayNits = dlg.nominalPeakBrightness();
568 }
569 d.linearizePolicy = LinearizePolicy::LinearFromHLG;
570 return TRC_LINEAR;
571 }
572 case JXL_TRANSFER_FUNCTION_DCI: {
573 dbgFile << "linearizing from SMPTE 428";
574 d.linearizePolicy = LinearizePolicy::LinearFromSMPTE428;
575 return TRC_LINEAR;
576 }
577 case JXL_TRANSFER_FUNCTION_709:
578 return TRC_ITU_R_BT_709_5;
579 case JXL_TRANSFER_FUNCTION_SRGB:
580 return TRC_IEC_61966_2_1;
581 case JXL_TRANSFER_FUNCTION_GAMMA: {
582 // Using roughly the same logic in KoColorProfile.
583 const double estGamma = 1.0 / colorEncoding.gamma;
584 const double error = 0.0001;
585 // ICC v2 u8Fixed8Number calculation
586 // Or can be prequantized as 1.80078125, courtesy of Elle Stone
587 if ((std::fabs(estGamma - 1.8) < error) || (std::fabs(estGamma - (461.0 / 256.0)) < error)) {
588 return TRC_GAMMA_1_8;
589 } else if (std::fabs(estGamma - 2.2) < error) {
591 } else if (std::fabs(estGamma - (563.0 / 256.0)) < error) {
592 return TRC_A98;
593 } else if (std::fabs(estGamma - 2.4) < error) {
594 return TRC_GAMMA_2_4;
595 } else if (std::fabs(estGamma - 2.8) < error) {
597 } else {
598 warnFile << "Found custom estimated gamma value for JXL color space" << estGamma;
599 return TRC_UNSPECIFIED;
600 }
601 }
602 case JXL_TRANSFER_FUNCTION_LINEAR:
603 return TRC_LINEAR;
604 case JXL_TRANSFER_FUNCTION_UNKNOWN:
605 default:
606 warnFile << "Found unknown OETF";
607 return TRC_UNSPECIFIED;
608 }
609 }();
610
611 const ColorPrimaries colorPrimaries = [&]() {
612 switch (colorEncoding.primaries) {
613 case JXL_PRIMARIES_SRGB:
615 case JXL_PRIMARIES_2100:
617 case JXL_PRIMARIES_P3:
619 default:
621 }
622 }();
623
624 const QVector<double> colorants = [&]() -> QVector<double> {
625 if (colorEncoding.primaries != JXL_PRIMARIES_CUSTOM) {
626 return {};
627 } else {
628 return {colorEncoding.white_point_xy[0],
629 colorEncoding.white_point_xy[1],
630 colorEncoding.primaries_red_xy[0],
631 colorEncoding.primaries_red_xy[1],
632 colorEncoding.primaries_green_xy[0],
633 colorEncoding.primaries_green_xy[1],
634 colorEncoding.primaries_blue_xy[0],
635 colorEncoding.primaries_blue_xy[1]};
636 }
637 }();
638
639 if (colorEncoding.rendering_intent == JXL_RENDERING_INTENT_PERCEPTUAL) {
641 } else if (colorEncoding.rendering_intent == JXL_RENDERING_INTENT_RELATIVE) {
643 } else if (colorEncoding.rendering_intent == JXL_RENDERING_INTENT_ABSOLUTE) {
645 } else if (colorEncoding.rendering_intent == JXL_RENDERING_INTENT_SATURATION) {
647 } else {
648 warnFile << "Cannot determine color rendering intent, set to Perceptual instead";
650 }
651
652 KoColorProfileQuery query(colorPrimaries, transferFunction);
653 profile = KoColorSpaceRegistry::instance()->profileFor(query);
654
655 dbgFile << "CICP profile data:" << colorants << colorPrimaries << transferFunction;
656
657 if (profile) {
658 dbgFile << "JXL CICP profile found" << profile->name();
659
660 if (d.linearizePolicy != LinearizePolicy::KeepTheSame) {
661 // Override output format!
662 d.m_depthID = Float32BitsColorDepthID;
663 d.m_pixelFormat.data_type = d.m_pixelFormat_target.data_type;
664
665 // HDR is a special case because we need to linearize in-house
666 d.cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID.id(), d.m_depthID.id(), profile);
667 }
668 }
669 }
670
671 if (!d.cs) {
672 size_t iccSize = 0;
673 QByteArray iccProfile;
674 if (JXL_DEC_SUCCESS
675 != JxlDecoderGetICCProfileSize(dec.get(),
676#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0,9,0)
677 nullptr,
678#endif
679 JXL_COLOR_PROFILE_TARGET_DATA,
680 &iccSize)) {
681 errFile << "ICC profile size retrieval failed";
682 document->setErrorMessage(i18nc("JPEG-XL errors", "Unable to read the image profile."));
684 }
685 iccProfile.resize(static_cast<int>(iccSize));
686 if (JXL_DEC_SUCCESS
687 != JxlDecoderGetColorAsICCProfile(dec.get(),
688#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0,9,0)
689 nullptr,
690#endif
691 JXL_COLOR_PROFILE_TARGET_DATA,
692 reinterpret_cast<uint8_t *>(iccProfile.data()),
693 static_cast<size_t>(iccProfile.size()))) {
694 document->setErrorMessage(i18nc("JPEG-XL errors", "Unable to read the image profile."));
696 }
697
698 // Get original profile if XYB is used
699 size_t iccTargetSize = 0;
700 QByteArray iccTargetProfile;
701 if (!d.m_info.uses_original_profile) {
702 if (JXL_DEC_SUCCESS
703 != JxlDecoderGetICCProfileSize(dec.get(),
704#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0,9,0)
705 nullptr,
706#endif
707 JXL_COLOR_PROFILE_TARGET_ORIGINAL,
708 &iccTargetSize)) {
709 errFile << "ICC profile size retrieval failed";
710 document->setErrorMessage(i18nc("JPEG-XL errors", "Unable to read the image profile."));
712 }
713 iccTargetProfile.resize(static_cast<int>(iccTargetSize));
714 if (JXL_DEC_SUCCESS
715 != JxlDecoderGetColorAsICCProfile(dec.get(),
716#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0,9,0)
717 nullptr,
718#endif
719 JXL_COLOR_PROFILE_TARGET_ORIGINAL,
720 reinterpret_cast<uint8_t *>(iccTargetProfile.data()),
721 static_cast<size_t>(iccTargetProfile.size()))) {
722 document->setErrorMessage(i18nc("JPEG-XL errors", "Unable to read the image profile."));
724 }
725 }
726
727 if (iccTargetSize && (iccProfile != iccTargetProfile)) {
728 // If the icc target is not 0 and different than target data.
729 // Meaning that the JXL is in XYB format and needing to convert back to
730 // the original color profile.
731 //
732 // Here we need to provide an intermediate transform space in float to prevent
733 // gamut clipping to sRGB if the target depth is integer.
734 dbgFile << "XYB with color transform needed";
735 needColorTransform = true;
736 profile = KoColorSpaceRegistry::instance()->createColorProfile(d.m_colorID.id(),
737 d.m_depthID.id(),
738 iccProfile);
739 d.cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID.id(), d.m_depthID.id(), profile);
740 profileIntermediate = KoColorSpaceRegistry::instance()->createColorProfile(d.m_colorID_target.id(),
741 d.m_depthID.id(),
742 iccTargetProfile);
743 d.cs_intermediate = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID_target.id(),
744 d.m_depthID.id(),
745 profileIntermediate);
746 profileTarget = KoColorSpaceRegistry::instance()->createColorProfile(d.m_colorID_target.id(),
747 d.m_depthID_target.id(),
748 iccTargetProfile);
749 d.cs_target = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID_target.id(),
750 d.m_depthID_target.id(),
751 profileTarget);
752
753 // No need for intermediate transform on float since it won't get clipped.
754 if (!(d.m_depthID_target == Float16BitsColorDepthID
755 || d.m_depthID_target == Float32BitsColorDepthID)) {
756 needIntermediateTransform = true;
757 }
758 } else if (!d.m_info.uses_original_profile) {
759 // If XYB is used but the profiles are same, skip conversion.
760 // Also set the color depth target to default.
761 //
762 // Try to fetch profile from CICP first...
763 dbgFile << "XYB without color transform needed";
764 needColorTransform = false;
765 d.m_depthID = d.m_depthID_target;
766 d.m_colorID = d.m_colorID_target;
767 d.m_pixelFormat.data_type = d.m_pixelFormat_target.data_type;
768 d.cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID.id(), d.m_depthID.id(), profile);
769
770 // ...or use ICC instead if CICP fetch failed.
771 if (!d.cs) {
772 dbgFile << "JXL CICP data couldn't be handled, falling back to ICC profile retrieval";
773 profile = KoColorSpaceRegistry::instance()->createColorProfile(d.m_colorID_target.id(),
774 d.m_depthID_target.id(),
775 iccProfile);
776 d.cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID_target.id(),
777 d.m_depthID_target.id(),
778 profile);
779 }
780 } else {
781 // Skip conversion on original profile.
782 dbgFile << "Original without color transform needed";
783 needColorTransform = false;
784 profile = KoColorSpaceRegistry::instance()->createColorProfile(d.m_colorID.id(),
785 d.m_depthID.id(),
786 iccProfile);
787 d.cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID.id(), d.m_depthID.id(), profile);
788 }
789 }
790
791 if (d.cs_target) {
792 dbgFile << "Source profile:" << d.cs->profile()->name();
793 dbgFile << "Source space:" << d.cs->name() << d.cs->colorModelId() << d.cs->colorDepthId();
794 dbgFile << "Target profile:" << d.cs_target->profile()->name();
795 dbgFile << "Color space:" << d.cs_target->name() << d.cs_target->colorModelId()
796 << d.cs_target->colorDepthId();
797 } else {
798 dbgFile << "Color space:" << d.cs->name() << d.cs->colorModelId() << d.cs->colorDepthId();
799 }
800 dbgFile << "JXL depth" << d.m_pixelFormat.data_type;
801
802 d.lCoef = d.cs->lumaCoefficients();
803
804 image = new KisImage(document->createUndoStore(),
805 static_cast<int>(d.m_info.xsize),
806 static_cast<int>(d.m_info.ysize),
807 d.cs,
808 "JPEG-XL image");
809
810 layer = new KisPaintLayer(image, image->nextLayerName(), UCHAR_MAX);
811 } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
812 d.m_currentFrame = new KisPaintDevice(image->colorSpace());
813
814 // Use raw byte buffer instead of image callback
815 size_t rawSize = 0;
816 if (JXL_DEC_SUCCESS != JxlDecoderImageOutBufferSize(dec.get(), &d.m_pixelFormat, &rawSize)) {
817 qWarning() << "JxlDecoderImageOutBufferSize failed";
819 }
820 d.m_rawData.resize(rawSize);
821 if (JXL_DEC_SUCCESS
822 != JxlDecoderSetImageOutBuffer(dec.get(),
823 &d.m_pixelFormat,
824 reinterpret_cast<uint8_t *>(d.m_rawData.data()),
825 static_cast<size_t>(d.m_rawData.size()))) {
826 qWarning() << "JxlDecoderSetImageOutBuffer failed";
828 }
829
830 if (d.isCMYK) {
831 // Prepare planar buffer for key channel
832 size_t bufferSize = 0;
833 if (JXL_DEC_SUCCESS
834 != JxlDecoderExtraChannelBufferSize(dec.get(), &d.m_pixelFormat, &bufferSize, d.cmykChannelID)) {
835 errFile << "JxlDecoderExtraChannelBufferSize failed";
837 break;
838 }
839 d.kPlane.resize(bufferSize);
840 if (JXL_DEC_SUCCESS
841 != JxlDecoderSetExtraChannelBuffer(dec.get(),
842 &d.m_pixelFormat,
843 d.kPlane.data(),
844 bufferSize,
845 d.cmykChannelID)) {
846 errFile << "JxlDecoderSetExtraChannelBuffer failed";
848 break;
849 }
850 }
851 } else if (status == JXL_DEC_FRAME) {
852 if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec.get(), &d.m_header)) {
853 errFile << "JxlDecoderGetFrameHeader failed";
855 }
856
857 const JxlBlendMode blendMode = d.m_header.layer_info.blend_info.blendmode;
858
859 if (isMultilayer || isMultipage) {
860 QString layerName;
861 QByteArray layerNameRaw;
862 if (d.m_header.name_length) {
863 KIS_SAFE_ASSERT_RECOVER(d.m_header.name_length < std::numeric_limits<int>::max())
864 {
865 document->setErrorMessage(i18nc("JPEG-XL", "Invalid JPEG-XL layer name length"));
867 }
868 layerNameRaw.resize(static_cast<int>(d.m_header.name_length + 1));
869 if (JXL_DEC_SUCCESS
870 != JxlDecoderGetFrameName(dec.get(),
871 layerNameRaw.data(),
872 static_cast<size_t>(layerNameRaw.size()))) {
873 errFile << "JxlDecoderGetFrameName failed";
874 break;
875 }
876 dbgFile << "\tlayer name:" << QString(layerNameRaw);
877 layerName = QString(layerNameRaw);
878 } else {
879 layerName = QString("Layer");
880 }
881 // Set the first layer name (if any)
882 if (!bgLayerSet) {
883 if (!layerNameRaw.isEmpty()) {
884 layer->setName(layerName);
885 }
886 } else {
887 additionalLayers.emplace_back(new KisPaintLayer(image, layerName, UCHAR_MAX));
888 if (blendMode == JXL_BLEND_MULADD) {
889 additionalLayers.back()->setCompositeOpId(QString("add"));
890 }
891 }
892 }
893 } else if (status == JXL_DEC_FULL_IMAGE) {
894 // Parse raw data using existing callback function
896 const JxlLayerInfo layerInfo = d.m_header.layer_info;
897 const QRect layerBounds = QRect(static_cast<int>(layerInfo.crop_x0),
898 static_cast<int>(layerInfo.crop_y0),
899 static_cast<int>(layerInfo.xsize),
900 static_cast<int>(layerInfo.ysize));
901 if (isAnimated) {
902 dbgFile << "Importing frame @" << d.m_nextFrameTime
903 << d.m_header.duration;
904
905 // XXX: If frame header duration is set to 0xFFFFFFFF it indicates that the
906 // current animation page has ended. Since we didn't currently support
907 // multipage image/animation, this will dump the whole animation instead.
908 // Otherwise, it may cause a lockup when loading a multipage image.
909 const uint32_t frameDurationNorm = d.m_header.duration == 0xFFFFFFFF ? 1 : d.m_header.duration;
910 if (d.m_nextFrameTime == 0) {
911 dbgFile << "Animation detected, ticks per second:"
912 << d.m_info.animation.tps_numerator
913 << d.m_info.animation.tps_denominator;
914 // XXX: How many ticks per second (FPS)?
915 // If > 240, block the derivation-- it's a stock JXL and
916 // Krita only supports up to 240 FPS.
917 // We'll try to derive the framerate from the first frame
918 // instead.
919 int framerate =
920 std::lround(d.m_info.animation.tps_numerator
921 / static_cast<double>(
922 d.m_info.animation.tps_denominator));
923 if (framerate > 240) {
924 warnFile << "JXL ticks per second value exceeds 240, "
925 "approximating FPS from the duration of "
926 "the first frame";
927 document->setWarningMessage(
928 i18nc("JPEG-XL errors",
929 "The animation declares a frame rate of more "
930 "than 240 FPS."));
931 const int approximatedFramerate = std::lround(
932 1000.0 / static_cast<double>(d.m_header.duration));
933 d.m_durationFrameInTicks =
934 static_cast<int>(frameDurationNorm);
935 framerate = std::max(approximatedFramerate, 1);
936 } else {
937 d.m_durationFrameInTicks = 1;
938 }
939 dbgFile << "Framerate:" << framerate;
940 layer->enableAnimation();
941 image->animationInterface()->setDocumentRangeStartFrame(0);
942 image->animationInterface()->setFramerate(framerate);
943 }
944
945 const int currentFrameTime = std::lround(
946 static_cast<double>(d.m_nextFrameTime)
947 / static_cast<double>(d.m_durationFrameInTicks));
948
949 auto *channel = layer->getKeyframeChannel(KisKeyframeChannel::Raster.id(), true);
950 auto *frame = dynamic_cast<KisRasterKeyframeChannel *>(channel);
951 image->animationInterface()->setDocumentRangeEndFrame(
952 std::lround(static_cast<double>(d.m_nextFrameTime
953 + frameDurationNorm)
954 / static_cast<double>(d.m_durationFrameInTicks))
955 - 1);
956 frame->importFrame(currentFrameTime, d.m_currentFrame, nullptr);
957 d.m_nextFrameTime += static_cast<int>(frameDurationNorm);
958 } else {
959 if (d.isCMYK && d.m_info.uses_original_profile) {
960 QVector<quint8 *> planes = d.m_currentFrame->readPlanarBytes(layerBounds.x(),
961 layerBounds.y(),
962 layerBounds.width(),
963 layerBounds.height());
964
965 // Planar buffer insertion for key channel
966 planes[3] = reinterpret_cast<quint8 *>(d.kPlane.data());
967 d.m_currentFrame->writePlanarBytes(planes,
968 layerBounds.x(),
969 layerBounds.y(),
970 layerBounds.width(),
971 layerBounds.height());
972
973 // JPEG-XL decode outputs an inverted CMYK colors
974 // This one I took from kis_filter_test for inverting the colors..
975 const KisFilterSP f = KisFilterRegistry::instance()->value("invert");
976 KIS_ASSERT(f);
977 const KisFilterConfigurationSP kfc =
978 f->defaultConfiguration(KisGlobalResourcesInterface::instance());
979 KIS_ASSERT(kfc);
980 f->process(d.m_currentFrame, layerBounds, kfc->cloneWithResourcesSnapshot());
981 }
982 if (!bgLayerSet) {
983 layer->paintDevice()->makeCloneFrom(d.m_currentFrame, layerBounds);
984 bgLayerSet = true;
985 } else {
986 additionalLayers.back()->paintDevice()->makeCloneFrom(d.m_currentFrame, layerBounds);
987 }
988 }
989 } else if (status == JXL_DEC_SUCCESS || status == JXL_DEC_BOX) {
990 if (std::strlen(boxType.data()) != 0) {
991 // Release buffer and get its final size.
992 const auto availOut = JxlDecoderReleaseBoxBuffer(dec.get());
993 const int finalSize = box.size() - static_cast<int>(availOut);
994 // Only resize and write boxes if it's not empty.
995 // And only input metadata boxes while skipping other boxes.
996 QByteArray type = boxType.toLower();
997 if ((std::equal(exifTag.begin(), exifTag.end(), type.constBegin())
998 || std::equal(xmpTag.begin(), xmpTag.end(), type.constBegin()))
999 && finalSize != 0) {
1000 metadataBoxes.emplace(type, QByteArray(box.data(), finalSize));
1001 }
1002 // Preemptively zero the box type out to prevent dangling
1003 // boxes.
1004 boxType.fill('\0');
1005 }
1006 if (status == JXL_DEC_SUCCESS) {
1007 // All decoding successfully finished.
1008
1009 // Insert layer metadata if available (delayed
1010 // in case the boxes came before the BASIC_INFO event)
1011 for (auto &metaBox : metadataBoxes) {
1012 const QByteArray &type = metaBox.first;
1013 QByteArray &value = metaBox.second;
1014 QBuffer buf(&value);
1015 if (std::equal(exifTag.begin(), exifTag.end(), type.constBegin())) {
1016 dbgFile << "Loading EXIF data. Size: " << value.size();
1017
1018 const auto *backend =
1020 "exif");
1021
1022 backend->loadFrom(layer->metaData(), &buf);
1023 } else if (std::equal(xmpTag.begin(), xmpTag.end(), type.constBegin())) {
1024 dbgFile << "Loading XMP or IPTC data. Size: " << value.size();
1025
1026 const auto *xmpBackend =
1028 "xmp");
1029
1030 if (!xmpBackend->loadFrom(layer->metaData(), &buf)) {
1031 const KisMetaData::IOBackend *iptcBackend =
1033 "iptc");
1034 iptcBackend->loadFrom(layer->metaData(), &buf);
1035 }
1036 }
1037 }
1038
1039 // It's not required to call JxlDecoderReleaseInput(dec.get()) here since
1040 // the decoder will be destroyed.
1041 image->addNode(layer, image->rootLayer().data());
1042 // Slip additional layers into layer stack
1043 for (const KisLayerSP &addLayer : additionalLayers) {
1044 image->addNode(addLayer, image->rootLayer().data());
1045 }
1046 if (needColorTransform) {
1047 if (needIntermediateTransform) {
1048 dbgFile << "Transforming to intermediate color space";
1049 image->convertImageColorSpace(d.cs_intermediate,
1050 d.m_intent,
1052 image->waitForDone();
1053 }
1054 dbgFile << "Transforming to target color space";
1055 image->convertImageColorSpace(d.cs_target,
1056 d.m_intent,
1058 image->waitForDone();
1059 }
1060 document->setCurrentImage(image);
1061 return ImportExportCodes::OK;
1062 } else {
1063 if (JxlDecoderGetBoxType(dec.get(), boxType.data(), JXL_TRUE) != JXL_DEC_SUCCESS) {
1064 errFile << "JxlDecoderGetBoxType failed";
1066 }
1067 const QByteArray type = boxType.toLower();
1068 if (std::equal(exifTag.begin(), exifTag.end(), type.constBegin())
1069 || std::equal(xmpTag.begin(), xmpTag.end(), type.constBegin())) {
1070 if (JxlDecoderSetBoxBuffer(
1071 dec.get(),
1072 reinterpret_cast<uint8_t *>(box.data()),
1073 static_cast<size_t>(box.size()))
1074 != JXL_DEC_SUCCESS) {
1075 errFile << "JxlDecoderSetBoxBuffer failed";
1077 }
1078 } else {
1079 dbgFile << "Skipping box" << boxType.data();
1080 }
1081 }
1082 } else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
1083 // Update the box size if it was truncated in a previous buffering.
1084 boxSize = box.size();
1085 box.resize(boxSize * 2);
1086 // Release buffer before setting it up again
1087 JxlDecoderReleaseBoxBuffer(dec.get());
1088 if (JxlDecoderSetBoxBuffer(
1089 dec.get(),
1090 reinterpret_cast<uint8_t *>(box.data() + boxSize),
1091 static_cast<size_t>(box.size() - boxSize))
1092 != JXL_DEC_SUCCESS) {
1093 errFile << "JxlDecoderGetBoxType failed";
1095 }
1096 } else {
1097 errFile << "Unknown decoder status" << status;
1099 }
1100 }
1101
1102 return ImportExportCodes::OK;
1103}
1104
1105#include <JPEGXLImport.moc>
static constexpr std::array< char, 4 > exifTag
void imageOutCallback(JPEGXLImportData &d)
void generateCallbackWithPolicy(JPEGXLImportData &d)
float value(const T *src, size_t ch)
float linearizeValueAsNeeded(float value)
static constexpr std::array< char, 4 > xmpTag
void generateCallbackWithType(JPEGXLImportData &d)
void generateCallback(JPEGXLImportData &d)
void generateCallbackWithSwap(JPEGXLImportData &d)
qreal v
const KoID Float32BitsColorDepthID("F32", ki18n("32-bit float/channel"))
const KoID GrayAColorModelID("GRAYA", ki18n("Grayscale/Alpha"))
const KoID Float16BitsColorDepthID("F16", ki18n("16-bit float/channel"))
const KoID Integer8BitsColorDepthID("U8", ki18n("8-bit integer/channel"))
const KoID Integer16BitsColorDepthID("U16", ki18n("16-bit integer/channel"))
const KoID CMYKAColorModelID("CMYKA", ki18n("CMYK/Alpha"))
const KoID RGBAColorModelID("RGBA", ki18n("RGB/Alpha"))
ColorPrimaries
The colorPrimaries enum Enum of colorants, follows ITU H.273 for values 0 to 255, and has extra known...
@ PRIMARIES_ITU_R_BT_2020_2_AND_2100_0
@ PRIMARIES_UNSPECIFIED
@ PRIMARIES_SMPTE_RP_431_2
@ PRIMARIES_ITU_R_BT_709_5
TransferCharacteristics
The transferCharacteristics enum Enum of transfer characteristics, follows ITU H.273 for values 0 to ...
@ TRC_ITU_R_BT_470_6_SYSTEM_M
@ TRC_ITU_R_BT_470_6_SYSTEM_B_G
@ TRC_IEC_61966_2_1
@ TRC_ITU_R_BT_709_5
ALWAYS_INLINE void applyHLGOOTF(float *rgb, const double *lumaCoefficients, float gamma=1.2f, float nominalPeak=1000.0f) noexcept
ALWAYS_INLINE float removeHLGCurve(float x) noexcept
LinearizePolicy
The KoColorTransferFunctions class.
ALWAYS_INLINE float removeSMPTE_ST_428Curve(float x) noexcept
ALWAYS_INLINE float removeSmpte2048Curve(float x) noexcept
QVector< qreal > lCoef
KoColorConversionTransformation::Intent m_intent
std::vector< quint8 > kPlane
KisImportExportErrorCode convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP configuration=nullptr) override
JPEGXLImport(QObject *parent, const QVariantList &)
static KisFilterRegistry * instance()
static KisResourcesInterfaceSP instance()
The base class for import and export filters.
static const KoID Raster
virtual bool loadFrom(Store *store, QIODevice *ioDevice) const =0
static KisMetadataBackendRegistry * instance()
The KisRasterKeyframeChannel is a concrete KisKeyframeChannel subclass that stores and manages KisRas...
void importFrame(int time, KisPaintDeviceSP sourceDevice, KUndo2Command *parentCommand)
virtual quint32 alphaPos() const =0
virtual quint32 channelCount() const =0
virtual void fromNormalisedChannelsValue(quint8 *pixel, const QVector< float > &values) const =0
const T value(const QString &id) const
Definition KoID.h:30
K_PLUGIN_FACTORY_WITH_JSON(KritaASCCDLFactory, "kritaasccdl.json", registerPlugin< KritaASCCDL >();) KritaASCCDL
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_ASSERT_X(cond, where, what)
Definition kis_assert.h:40
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
#define warnFile
Definition kis_debug.h:95
#define errFile
Definition kis_debug.h:115
#define dbgFile
Definition kis_debug.h:53
The KoColorProfileQuery struct.
const KoColorSpace * colorSpace(const QString &colorModelId, const QString &colorDepthId, const KoColorProfile *profile)
static KoColorSpaceRegistry * instance()
const KoColorProfile * profileFor(const KoColorProfileQuery &query, const bool generate=true) const
profileFor tries to find the profile that matches these characteristics, if no such profile is found,...
const KoColorProfile * createColorProfile(const QString &colorModelId, const QString &colorDepthId, const QByteArray &rawData)