KImageFormats

heif.cpp
1/*
2 High Efficiency Image File Format (HEIF) support for QImage.
3
4 SPDX-FileCopyrightText: 2020 Sirius Bakke <sirius@bakke.co>
5 SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "heif_p.h"
11#include "util_p.h"
12#include <libheif/heif.h>
13
14#include <QColorSpace>
15#include <QDebug>
16#include <QPointF>
17#include <QSysInfo>
18#include <limits>
19#include <string.h>
20
21size_t HEIFHandler::m_initialized_count = 0;
22bool HEIFHandler::m_plugins_queried = false;
23bool HEIFHandler::m_heif_decoder_available = false;
24bool HEIFHandler::m_heif_encoder_available = false;
25bool HEIFHandler::m_hej2_decoder_available = false;
26bool HEIFHandler::m_avci_decoder_available = false;
27
28extern "C" {
29static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata)
30{
31 heif_error error;
32 error.code = heif_error_Ok;
33 error.subcode = heif_suberror_Unspecified;
34 error.message = "Success";
35
36 if (!userdata || !data || size == 0) {
37 error.code = heif_error_Usage_error;
38 error.subcode = heif_suberror_Null_pointer_argument;
39 error.message = "Wrong parameters!";
40 return error;
41 }
42
43 QIODevice *ioDevice = static_cast<QIODevice *>(userdata);
44 qint64 bytesWritten = ioDevice->write(static_cast<const char *>(data), size);
45
46 if (bytesWritten < static_cast<qint64>(size)) {
47 error.code = heif_error_Encoding_error;
48 error.message = "Bytes written to QIODevice are smaller than input data size";
49 error.subcode = heif_suberror_Cannot_write_output_data;
50 }
51
52 return error;
53}
54}
55
56HEIFHandler::HEIFHandler()
57 : m_parseState(ParseHeicNotParsed)
58 , m_quality(100)
59{
60}
61
62bool HEIFHandler::canRead() const
63{
64 if (m_parseState == ParseHeicNotParsed) {
65 QIODevice *dev = device();
66 if (dev) {
67 const QByteArray header = dev->peek(28);
68
69 if (HEIFHandler::isSupportedBMFFType(header)) {
70 setFormat("heif");
71 return true;
72 }
73
74 if (HEIFHandler::isSupportedHEJ2(header)) {
75 setFormat("hej2");
76 return true;
77 }
78
79 if (HEIFHandler::isSupportedAVCI(header)) {
80 setFormat("avci");
81 return true;
82 }
83 }
84 return false;
85 }
86
87 if (m_parseState != ParseHeicError) {
88 return true;
89 }
90 return false;
91}
92
93bool HEIFHandler::read(QImage *outImage)
94{
95 if (!ensureParsed()) {
96 return false;
97 }
98
99 *outImage = m_current_image;
100 return true;
101}
102
103bool HEIFHandler::write(const QImage &image)
104{
105 if (image.format() == QImage::Format_Invalid || image.isNull()) {
106 qWarning("No image data to save");
107 return false;
108 }
109
110#if LIBHEIF_HAVE_VERSION(1, 13, 0)
111 startHeifLib();
112#endif
113
114 bool success = write_helper(image);
115
116#if LIBHEIF_HAVE_VERSION(1, 13, 0)
117 finishHeifLib();
118#endif
119
120 return success;
121}
122
123bool HEIFHandler::write_helper(const QImage &image)
124{
125 int save_depth; // 8 or 10bit per channel
126 QImage::Format tmpformat; // format for temporary image
127 const bool save_alpha = image.hasAlphaChannel();
128
129 switch (image.format()) {
138 save_depth = 10;
139 break;
140 default:
141 if (image.depth() > 32) {
142 save_depth = 10;
143 } else {
144 save_depth = 8;
145 }
146 break;
147 }
148
149 heif_chroma chroma;
150 if (save_depth > 8) {
151 if (save_alpha) {
152 tmpformat = QImage::Format_RGBA64;
153 chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
154 } else {
155 tmpformat = QImage::Format_RGBX64;
156 chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
157 }
158 } else {
159 if (save_alpha) {
160 tmpformat = QImage::Format_RGBA8888;
161 chroma = heif_chroma_interleaved_RGBA;
162 } else {
163 tmpformat = QImage::Format_RGB888;
164 chroma = heif_chroma_interleaved_RGB;
165 }
166 }
167
168#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
169 QImage tmpimage;
170 auto cs = image.colorSpace();
171 if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
172 tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), tmpformat);
173 } else if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Gray
175 QColorSpace::TransferFunction trc_new = cs.transferFunction();
176 float gamma_new = cs.gamma();
177 if (trc_new == QColorSpace::TransferFunction::Custom) {
178 trc_new = QColorSpace::TransferFunction::SRgb;
179 }
180 tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, trc_new, gamma_new), tmpformat);
181 } else {
182 tmpimage = image.convertToFormat(tmpformat);
183 }
184#else
185 QImage tmpimage = image.convertToFormat(tmpformat);
186#endif
187
188 struct heif_context *context = heif_context_alloc();
189 struct heif_error err;
190 struct heif_image *h_image = nullptr;
191
192 err = heif_image_create(tmpimage.width(), tmpimage.height(), heif_colorspace_RGB, chroma, &h_image);
193 if (err.code) {
194 qWarning() << "heif_image_create error:" << err.message;
195 heif_context_free(context);
196 return false;
197 }
198
199 QByteArray iccprofile = tmpimage.colorSpace().iccProfile();
200 if (iccprofile.size() > 0) {
201 heif_image_set_raw_color_profile(h_image, "prof", iccprofile.constData(), iccprofile.size());
202 }
203
204 heif_image_add_plane(h_image, heif_channel_interleaved, image.width(), image.height(), save_depth);
205 int stride = 0;
206 uint8_t *const dst = heif_image_get_plane(h_image, heif_channel_interleaved, &stride);
207 size_t rowbytes;
208
209 switch (save_depth) {
210 case 10:
211 if (save_alpha) {
212 for (int y = 0; y < tmpimage.height(); y++) {
213 const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
214 uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
215 for (int x = 0; x < tmpimage.width(); x++) {
216 int tmp_pixelval;
217 // R
218 tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
219 *dest_word = qBound(0, tmp_pixelval, 1023);
220 src_word++;
221 dest_word++;
222 // G
223 tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
224 *dest_word = qBound(0, tmp_pixelval, 1023);
225 src_word++;
226 dest_word++;
227 // B
228 tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
229 *dest_word = qBound(0, tmp_pixelval, 1023);
230 src_word++;
231 dest_word++;
232 // A
233 tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
234 *dest_word = qBound(0, tmp_pixelval, 1023);
235 src_word++;
236 dest_word++;
237 }
238 }
239 } else { // no alpha channel
240 for (int y = 0; y < tmpimage.height(); y++) {
241 const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
242 uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
243 for (int x = 0; x < tmpimage.width(); x++) {
244 int tmp_pixelval;
245 // R
246 tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
247 *dest_word = qBound(0, tmp_pixelval, 1023);
248 src_word++;
249 dest_word++;
250 // G
251 tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
252 *dest_word = qBound(0, tmp_pixelval, 1023);
253 src_word++;
254 dest_word++;
255 // B
256 tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
257 *dest_word = qBound(0, tmp_pixelval, 1023);
258 src_word++;
259 dest_word++;
260 // X
261 src_word++;
262 }
263 }
264 }
265 break;
266 case 8:
267 rowbytes = save_alpha ? (tmpimage.width() * 4) : (tmpimage.width() * 3);
268 for (int y = 0; y < tmpimage.height(); y++) {
269 memcpy(dst + (y * stride), tmpimage.constScanLine(y), rowbytes);
270 }
271 break;
272 default:
273 qWarning() << "Unsupported depth:" << save_depth;
274 heif_image_release(h_image);
275 heif_context_free(context);
276 return false;
277 break;
278 }
279
280 struct heif_encoder *encoder = nullptr;
281 err = heif_context_get_encoder_for_format(context, heif_compression_HEVC, &encoder);
282 if (err.code) {
283 qWarning() << "Unable to get an encoder instance:" << err.message;
284 heif_image_release(h_image);
285 heif_context_free(context);
286 return false;
287 }
288
289 heif_encoder_set_lossy_quality(encoder, m_quality);
290 if (m_quality > 90) {
291 if (m_quality == 100) {
292 heif_encoder_set_lossless(encoder, true);
293 }
294 heif_encoder_set_parameter_string(encoder, "chroma", "444");
295 }
296
297 struct heif_encoding_options *encoder_options = heif_encoding_options_alloc();
298 encoder_options->save_alpha_channel = save_alpha;
299
300 if ((tmpimage.width() % 2 == 1) || (tmpimage.height() % 2 == 1)) {
301 qWarning() << "Image has odd dimension!\nUse even-numbered dimension(s) for better compatibility with other HEIF implementations.";
302 if (save_alpha) {
303 // This helps to save alpha channel when image has odd dimension
304 encoder_options->macOS_compatibility_workaround = 0;
305 }
306 }
307
308 err = heif_context_encode_image(context, h_image, encoder, encoder_options, nullptr);
309
310 if (encoder_options) {
311 heif_encoding_options_free(encoder_options);
312 }
313
314 if (err.code) {
315 qWarning() << "heif_context_encode_image failed:" << err.message;
316 heif_encoder_release(encoder);
317 heif_image_release(h_image);
318 heif_context_free(context);
319 return false;
320 }
321
322 struct heif_writer writer;
323 writer.writer_api_version = 1;
324 writer.write = heifhandler_write_callback;
325
326 err = heif_context_write(context, &writer, device());
327
328 heif_encoder_release(encoder);
329 heif_image_release(h_image);
330
331 if (err.code) {
332 qWarning() << "Writing HEIF image failed:" << err.message;
333 heif_context_free(context);
334 return false;
335 }
336
337 heif_context_free(context);
338 return true;
339}
340
341bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
342{
343 if (header.size() < 28) {
344 return false;
345 }
346
347 const char *buffer = header.constData();
348 if (qstrncmp(buffer + 4, "ftyp", 4) == 0) {
349 if (qstrncmp(buffer + 8, "heic", 4) == 0) {
350 return true;
351 }
352 if (qstrncmp(buffer + 8, "heis", 4) == 0) {
353 return true;
354 }
355 if (qstrncmp(buffer + 8, "heix", 4) == 0) {
356 return true;
357 }
358
359 /* we want to avoid loading AVIF files via this plugin */
360 if (qstrncmp(buffer + 8, "mif1", 4) == 0) {
361 for (int offset = 16; offset <= 24; offset += 4) {
362 if (qstrncmp(buffer + offset, "avif", 4) == 0) {
363 return false;
364 }
365 }
366 return true;
367 }
368
369 if (qstrncmp(buffer + 8, "mif2", 4) == 0) {
370 return true;
371 }
372 if (qstrncmp(buffer + 8, "msf1", 4) == 0) {
373 return true;
374 }
375 }
376
377 return false;
378}
379
380bool HEIFHandler::isSupportedHEJ2(const QByteArray &header)
381{
382 if (header.size() < 28) {
383 return false;
384 }
385
386 const char *buffer = header.constData();
387 if (qstrncmp(buffer + 4, "ftyp", 4) == 0) {
388 if (qstrncmp(buffer + 8, "j2ki", 4) == 0) {
389 return true;
390 }
391 }
392
393 return false;
394}
395
396bool HEIFHandler::isSupportedAVCI(const QByteArray &header)
397{
398 if (header.size() < 28) {
399 return false;
400 }
401
402 const char *buffer = header.constData();
403 if (qstrncmp(buffer + 4, "ftyp", 4) == 0) {
404 if (qstrncmp(buffer + 8, "avci", 4) == 0) {
405 return true;
406 }
407 }
408
409 return false;
410}
411
412QVariant HEIFHandler::option(ImageOption option) const
413{
414 if (option == Quality) {
415 return m_quality;
416 }
417
418 if (!supportsOption(option) || !ensureParsed()) {
419 return QVariant();
420 }
421
422 switch (option) {
423 case Size:
424 return m_current_image.size();
425 break;
426 default:
427 return QVariant();
428 break;
429 }
430}
431
432void HEIFHandler::setOption(ImageOption option, const QVariant &value)
433{
434 switch (option) {
435 case Quality:
436 m_quality = value.toInt();
437 if (m_quality > 100) {
438 m_quality = 100;
439 } else if (m_quality < 0) {
440 m_quality = 100;
441 }
442 break;
443 default:
444 QImageIOHandler::setOption(option, value);
445 break;
446 }
447}
448
449bool HEIFHandler::supportsOption(ImageOption option) const
450{
451 return option == Quality || option == Size;
452}
453
454bool HEIFHandler::ensureParsed() const
455{
456 if (m_parseState == ParseHeicSuccess) {
457 return true;
458 }
459 if (m_parseState == ParseHeicError) {
460 return false;
461 }
462
463 HEIFHandler *that = const_cast<HEIFHandler *>(this);
464
465#if LIBHEIF_HAVE_VERSION(1, 13, 0)
466 startHeifLib();
467#endif
468
469 bool success = that->ensureDecoder();
470
471#if LIBHEIF_HAVE_VERSION(1, 13, 0)
472 finishHeifLib();
473#endif
474 return success;
475}
476
477bool HEIFHandler::ensureDecoder()
478{
479 if (m_parseState != ParseHeicNotParsed) {
480 if (m_parseState == ParseHeicSuccess) {
481 return true;
482 }
483 return false;
484 }
485
486 const QByteArray buffer = device()->readAll();
487 if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer) && !HEIFHandler::isSupportedAVCI(buffer)) {
488 m_parseState = ParseHeicError;
489 return false;
490 }
491
492 struct heif_context *ctx = heif_context_alloc();
493 struct heif_error err = heif_context_read_from_memory(ctx, static_cast<const void *>(buffer.constData()), buffer.size(), nullptr);
494
495 if (err.code) {
496 qWarning() << "heif_context_read_from_memory error:" << err.message;
497 heif_context_free(ctx);
498 m_parseState = ParseHeicError;
499 return false;
500 }
501
502 struct heif_image_handle *handle = nullptr;
503 err = heif_context_get_primary_image_handle(ctx, &handle);
504 if (err.code) {
505 qWarning() << "heif_context_get_primary_image_handle error:" << err.message;
506 heif_context_free(ctx);
507 m_parseState = ParseHeicError;
508 return false;
509 }
510
511 if ((heif_image_handle_get_width(handle) == 0) || (heif_image_handle_get_height(handle) == 0)) {
512 m_parseState = ParseHeicError;
513 heif_image_handle_release(handle);
514 heif_context_free(ctx);
515 qWarning() << "HEIC image has zero dimension";
516 return false;
517 }
518
519 const int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle);
520
521 if (bit_depth < 8) {
522 m_parseState = ParseHeicError;
523 heif_image_handle_release(handle);
524 heif_context_free(ctx);
525 qWarning() << "HEIF image with undefined or unsupported bit depth.";
526 return false;
527 }
528
529 const bool hasAlphaChannel = heif_image_handle_has_alpha_channel(handle);
530 heif_chroma chroma;
531
532 QImage::Format target_image_format;
533
534 if (bit_depth == 10 || bit_depth == 12) {
535 if (hasAlphaChannel) {
536 chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
537 target_image_format = QImage::Format_RGBA64;
538 } else {
539 chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
540 target_image_format = QImage::Format_RGBX64;
541 }
542 } else if (bit_depth == 8) {
543 if (hasAlphaChannel) {
544 chroma = heif_chroma_interleaved_RGBA;
545 target_image_format = QImage::Format_ARGB32;
546 } else {
547 chroma = heif_chroma_interleaved_RGB;
548 target_image_format = QImage::Format_RGB32;
549 }
550 } else {
551 m_parseState = ParseHeicError;
552 heif_image_handle_release(handle);
553 heif_context_free(ctx);
554 qWarning() << "Unsupported bit depth:" << bit_depth;
555 return false;
556 }
557
558 struct heif_decoding_options *decoder_option = heif_decoding_options_alloc();
559
560#if LIBHEIF_HAVE_VERSION(1, 13, 0)
561 decoder_option->strict_decoding = 1;
562#endif
563
564 struct heif_image *img = nullptr;
565 err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
566
567#if LIBHEIF_HAVE_VERSION(1, 13, 0)
568 if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr && buffer.contains("Xiaomi")) {
569 qWarning() << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!";
570
571 // second try to decode with strict decoding disabled
572 decoder_option->strict_decoding = 0;
573 err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
574 }
575#endif
576
577 if (decoder_option) {
578 heif_decoding_options_free(decoder_option);
579 }
580
581 if (err.code) {
582 qWarning() << "heif_decode_image error:" << err.message;
583 heif_image_handle_release(handle);
584 heif_context_free(ctx);
585 m_parseState = ParseHeicError;
586 return false;
587 }
588
589 const int imageWidth = heif_image_get_width(img, heif_channel_interleaved);
590 const int imageHeight = heif_image_get_height(img, heif_channel_interleaved);
591
592 QSize imageSize(imageWidth, imageHeight);
593
594 if (!imageSize.isValid()) {
595 heif_image_release(img);
596 heif_image_handle_release(handle);
597 heif_context_free(ctx);
598 m_parseState = ParseHeicError;
599 qWarning() << "HEIC image size invalid:" << imageSize;
600 return false;
601 }
602
603 int stride = 0;
604 const uint8_t *const src = heif_image_get_plane_readonly(img, heif_channel_interleaved, &stride);
605
606 if (!src || stride <= 0) {
607 heif_image_release(img);
608 heif_image_handle_release(handle);
609 heif_context_free(ctx);
610 m_parseState = ParseHeicError;
611 qWarning() << "HEIC data pixels information not valid!";
612 return false;
613 }
614
615 m_current_image = imageAlloc(imageSize, target_image_format);
616 if (m_current_image.isNull()) {
617 heif_image_release(img);
618 heif_image_handle_release(handle);
619 heif_context_free(ctx);
620 m_parseState = ParseHeicError;
621 qWarning() << "Unable to allocate memory!";
622 return false;
623 }
624
625 switch (bit_depth) {
626 case 12:
627 if (hasAlphaChannel) {
628 for (int y = 0; y < imageHeight; y++) {
629 const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
630 uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
631 for (int x = 0; x < imageWidth; x++) {
632 int tmpvalue;
633 // R
634 tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
635 tmpvalue = qBound(0, tmpvalue, 65535);
636 *dest_data = (uint16_t)tmpvalue;
637 src_word++;
638 dest_data++;
639 // G
640 tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
641 tmpvalue = qBound(0, tmpvalue, 65535);
642 *dest_data = (uint16_t)tmpvalue;
643 src_word++;
644 dest_data++;
645 // B
646 tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
647 tmpvalue = qBound(0, tmpvalue, 65535);
648 *dest_data = (uint16_t)tmpvalue;
649 src_word++;
650 dest_data++;
651 // A
652 tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
653 tmpvalue = qBound(0, tmpvalue, 65535);
654 *dest_data = (uint16_t)tmpvalue;
655 src_word++;
656 dest_data++;
657 }
658 }
659 } else { // no alpha channel
660 for (int y = 0; y < imageHeight; y++) {
661 const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
662 uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
663 for (int x = 0; x < imageWidth; x++) {
664 int tmpvalue;
665 // R
666 tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
667 tmpvalue = qBound(0, tmpvalue, 65535);
668 *dest_data = (uint16_t)tmpvalue;
669 src_word++;
670 dest_data++;
671 // G
672 tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
673 tmpvalue = qBound(0, tmpvalue, 65535);
674 *dest_data = (uint16_t)tmpvalue;
675 src_word++;
676 dest_data++;
677 // B
678 tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
679 tmpvalue = qBound(0, tmpvalue, 65535);
680 *dest_data = (uint16_t)tmpvalue;
681 src_word++;
682 dest_data++;
683 // X = 0xffff
684 *dest_data = 0xffff;
685 dest_data++;
686 }
687 }
688 }
689 break;
690 case 10:
691 if (hasAlphaChannel) {
692 for (int y = 0; y < imageHeight; y++) {
693 const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
694 uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
695 for (int x = 0; x < imageWidth; x++) {
696 int tmpvalue;
697 // R
698 tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
699 tmpvalue = qBound(0, tmpvalue, 65535);
700 *dest_data = (uint16_t)tmpvalue;
701 src_word++;
702 dest_data++;
703 // G
704 tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
705 tmpvalue = qBound(0, tmpvalue, 65535);
706 *dest_data = (uint16_t)tmpvalue;
707 src_word++;
708 dest_data++;
709 // B
710 tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
711 tmpvalue = qBound(0, tmpvalue, 65535);
712 *dest_data = (uint16_t)tmpvalue;
713 src_word++;
714 dest_data++;
715 // A
716 tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
717 tmpvalue = qBound(0, tmpvalue, 65535);
718 *dest_data = (uint16_t)tmpvalue;
719 src_word++;
720 dest_data++;
721 }
722 }
723 } else { // no alpha channel
724 for (int y = 0; y < imageHeight; y++) {
725 const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
726 uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
727 for (int x = 0; x < imageWidth; x++) {
728 int tmpvalue;
729 // R
730 tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
731 tmpvalue = qBound(0, tmpvalue, 65535);
732 *dest_data = (uint16_t)tmpvalue;
733 src_word++;
734 dest_data++;
735 // G
736 tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
737 tmpvalue = qBound(0, tmpvalue, 65535);
738 *dest_data = (uint16_t)tmpvalue;
739 src_word++;
740 dest_data++;
741 // B
742 tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
743 tmpvalue = qBound(0, tmpvalue, 65535);
744 *dest_data = (uint16_t)tmpvalue;
745 src_word++;
746 dest_data++;
747 // X = 0xffff
748 *dest_data = 0xffff;
749 dest_data++;
750 }
751 }
752 }
753 break;
754 case 8:
755 if (hasAlphaChannel) {
756 for (int y = 0; y < imageHeight; y++) {
757 const uint8_t *src_byte = src + (y * stride);
758 uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
759 for (int x = 0; x < imageWidth; x++) {
760 int red = *src_byte++;
761 int green = *src_byte++;
762 int blue = *src_byte++;
763 int alpha = *src_byte++;
764 *dest_pixel = qRgba(red, green, blue, alpha);
765 dest_pixel++;
766 }
767 }
768 } else { // no alpha channel
769 for (int y = 0; y < imageHeight; y++) {
770 const uint8_t *src_byte = src + (y * stride);
771 uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
772 for (int x = 0; x < imageWidth; x++) {
773 int red = *src_byte++;
774 int green = *src_byte++;
775 int blue = *src_byte++;
776 *dest_pixel = qRgb(red, green, blue);
777 dest_pixel++;
778 }
779 }
780 }
781 break;
782 default:
783 heif_image_release(img);
784 heif_image_handle_release(handle);
785 heif_context_free(ctx);
786 m_parseState = ParseHeicError;
787 qWarning() << "Unsupported bit depth:" << bit_depth;
788 return false;
789 break;
790 }
791
792 heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle);
793 if (profileType == heif_color_profile_type_prof || profileType == heif_color_profile_type_rICC) {
794 size_t rawProfileSize = heif_image_handle_get_raw_color_profile_size(handle);
795 if (rawProfileSize > 0 && rawProfileSize < std::numeric_limits<int>::max()) {
796 QByteArray ba(rawProfileSize, 0);
797 err = heif_image_handle_get_raw_color_profile(handle, ba.data());
798 if (err.code) {
799 qWarning() << "icc profile loading failed";
800 } else {
801 m_current_image.setColorSpace(QColorSpace::fromIccProfile(ba));
802 if (!m_current_image.colorSpace().isValid()) {
803 qWarning() << "HEIC image has Qt-unsupported or invalid ICC profile!";
804 }
805 }
806 } else {
807 qWarning() << "icc profile is empty or above limits";
808 }
809
810 } else if (profileType == heif_color_profile_type_nclx) {
811 struct heif_color_profile_nclx *nclx = nullptr;
812 err = heif_image_handle_get_nclx_color_profile(handle, &nclx);
813 if (err.code || !nclx) {
814 qWarning() << "nclx profile loading failed";
815 } else {
816 const QPointF redPoint(nclx->color_primary_red_x, nclx->color_primary_red_y);
817 const QPointF greenPoint(nclx->color_primary_green_x, nclx->color_primary_green_y);
818 const QPointF bluePoint(nclx->color_primary_blue_x, nclx->color_primary_blue_y);
819 const QPointF whitePoint(nclx->color_primary_white_x, nclx->color_primary_white_y);
820
821 QColorSpace::TransferFunction q_trc = QColorSpace::TransferFunction::Custom;
822 float q_trc_gamma = 0.0f;
823
824 switch (nclx->transfer_characteristics) {
825 case 4:
826 q_trc = QColorSpace::TransferFunction::Gamma;
827 q_trc_gamma = 2.2f;
828 break;
829 case 5:
830 q_trc = QColorSpace::TransferFunction::Gamma;
831 q_trc_gamma = 2.8f;
832 break;
833 case 8:
834 q_trc = QColorSpace::TransferFunction::Linear;
835 break;
836 case 2:
837 case 13:
838 q_trc = QColorSpace::TransferFunction::SRgb;
839 break;
840#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0))
841 case 16:
842 q_trc = QColorSpace::TransferFunction::St2084;
843 break;
844 case 18:
845 q_trc = QColorSpace::TransferFunction::Hlg;
846 break;
847#endif
848 default:
849 qWarning("CICP color_primaries: %d, transfer_characteristics: %d\nThe colorspace is unsupported by this plug-in yet.",
850 nclx->color_primaries,
851 nclx->transfer_characteristics);
852 q_trc = QColorSpace::TransferFunction::SRgb;
853 break;
854 }
855
856 if (q_trc != QColorSpace::TransferFunction::Custom) { // we create new colorspace using Qt
857 switch (nclx->color_primaries) {
858 case 1:
859 case 2:
860 m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma));
861 break;
862 case 12:
863 m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma));
864 break;
865 default:
866 m_current_image.setColorSpace(QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma));
867 break;
868 }
869 }
870 heif_nclx_color_profile_free(nclx);
871
872 if (!m_current_image.colorSpace().isValid()) {
873 qWarning() << "HEIC plugin created invalid QColorSpace from NCLX!";
874 }
875 }
876
877 } else {
878 m_current_image.setColorSpace(QColorSpace(QColorSpace::SRgb));
879 }
880
881 heif_image_release(img);
882 heif_image_handle_release(handle);
883 heif_context_free(ctx);
884 m_parseState = ParseHeicSuccess;
885 return true;
886}
887
888bool HEIFHandler::isHeifDecoderAvailable()
889{
890 HEIFHandler::queryHeifLib();
891
892 return m_heif_decoder_available;
893}
894
895bool HEIFHandler::isHeifEncoderAvailable()
896{
897 HEIFHandler::queryHeifLib();
898
899 return m_heif_encoder_available;
900}
901
902bool HEIFHandler::isHej2DecoderAvailable()
903{
904 HEIFHandler::queryHeifLib();
905
906 return m_hej2_decoder_available;
907}
908
909bool HEIFHandler::isAVCIDecoderAvailable()
910{
911 HEIFHandler::queryHeifLib();
912
913 return m_avci_decoder_available;
914}
915
916void HEIFHandler::queryHeifLib()
917{
918 QMutexLocker locker(&getHEIFHandlerMutex());
919
920 if (!m_plugins_queried) {
921#if LIBHEIF_HAVE_VERSION(1, 13, 0)
922 if (m_initialized_count == 0) {
923 heif_init(nullptr);
924 }
925#endif
926
927 m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
928 m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
929#if LIBHEIF_HAVE_VERSION(1, 13, 0)
930 m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
931#endif
932#if LIBHEIF_HAVE_VERSION(1, 19, 0)
933 m_avci_decoder_available = heif_have_decoder_for_format(heif_compression_AVC);
934#endif
935 m_plugins_queried = true;
936
937#if LIBHEIF_HAVE_VERSION(1, 13, 0)
938 if (m_initialized_count == 0) {
939 heif_deinit();
940 }
941#endif
942 }
943}
944
945void HEIFHandler::startHeifLib()
946{
947#if LIBHEIF_HAVE_VERSION(1, 13, 0)
948 QMutexLocker locker(&getHEIFHandlerMutex());
949
950 if (m_initialized_count == 0) {
951 heif_init(nullptr);
952 }
953
954 m_initialized_count++;
955#endif
956}
957
958void HEIFHandler::finishHeifLib()
959{
960#if LIBHEIF_HAVE_VERSION(1, 13, 0)
961 QMutexLocker locker(&getHEIFHandlerMutex());
962
963 if (m_initialized_count == 0) {
964 return;
965 }
966
967 m_initialized_count--;
968 if (m_initialized_count == 0) {
969 heif_deinit();
970 }
971
972#endif
973}
974
975QMutex &HEIFHandler::getHEIFHandlerMutex()
976{
977 static QMutex heif_handler_mutex;
978 return heif_handler_mutex;
979}
980
981QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
982{
983 if (format == "heif" || format == "heic") {
984 Capabilities format_cap;
985 if (HEIFHandler::isHeifDecoderAvailable()) {
986 format_cap |= CanRead;
987 }
988 if (HEIFHandler::isHeifEncoderAvailable()) {
989 format_cap |= CanWrite;
990 }
991 return format_cap;
992 }
993
994 if (format == "hej2") {
995 Capabilities format_cap;
996 if (HEIFHandler::isHej2DecoderAvailable()) {
997 format_cap |= CanRead;
998 }
999 return format_cap;
1000 }
1001
1002 if (format == "avci") {
1003 Capabilities format_cap;
1004 if (HEIFHandler::isAVCIDecoderAvailable()) {
1005 format_cap |= CanRead;
1006 }
1007 return format_cap;
1008 }
1009
1010 if (!format.isEmpty()) {
1011 return {};
1012 }
1013 if (!device->isOpen()) {
1014 return {};
1015 }
1016
1017 Capabilities cap;
1018 if (device->isReadable()) {
1019 const QByteArray header = device->peek(28);
1020
1021 if ((HEIFHandler::isSupportedBMFFType(header) && HEIFHandler::isHeifDecoderAvailable())
1022 || (HEIFHandler::isSupportedHEJ2(header) && HEIFHandler::isHej2DecoderAvailable())
1023 || (HEIFHandler::isSupportedAVCI(header) && HEIFHandler::isAVCIDecoderAvailable())) {
1024 cap |= CanRead;
1025 }
1026 }
1027
1028 if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) {
1029 cap |= CanWrite;
1030 }
1031 return cap;
1032}
1033
1034QImageIOHandler *HEIFPlugin::create(QIODevice *device, const QByteArray &format) const
1035{
1036 QImageIOHandler *handler = new HEIFHandler;
1037 handler->setDevice(device);
1038 handler->setFormat(format);
1039 return handler;
1040}
1041
1042#include "moc_heif_p.cpp"
KGUIADDONS_EXPORT qreal chroma(const QColor &)
QFlags< Capability > Capabilities
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
const char * constData() const const
bool contains(QByteArrayView bv) const const
bool isEmpty() const const
qsizetype size() const const
QColorSpace fromIccProfile(const QByteArray &iccProfile)
QByteArray iccProfile() const const
QColorSpace colorSpace() const const
const uchar * constScanLine(int i) const const
QImage convertToFormat(Format format, Qt::ImageConversionFlags flags) &&
QImage convertedToColorSpace(const QColorSpace &colorSpace) const const
int depth() const const
Format format() const const
bool hasAlphaChannel() const const
int height() const const
bool isNull() const const
int width() const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
virtual void setOption(ImageOption option, const QVariant &value)
bool isOpen() const const
bool isReadable() const const
bool isWritable() const const
QByteArray peek(qint64 maxSize)
qint64 write(const QByteArray &data)
int toInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 3 2025 12:01:07 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.