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.1-or-later |
6 | */ |
7 | |
8 | #include "jp2_p.h" |
9 | #include "scanlineconverter_p.h" |
10 | #include "util_p.h" |
11 | |
12 | #include <QColorSpace> |
13 | #include <QIODevice> |
14 | #include <QImage> |
15 | #include <QImageReader> |
16 | #include <QThread> |
17 | |
18 | #include <openjpeg.h> |
19 | |
20 | /* *** JP2_MAX_IMAGE_WIDTH and JP2_MAX_IMAGE_HEIGHT *** |
21 | * The maximum size in pixel allowed by the plugin. |
22 | */ |
23 | #ifndef JP2_MAX_IMAGE_WIDTH |
24 | #define JP2_MAX_IMAGE_WIDTH 300000 |
25 | #endif |
26 | #ifndef JP2_MAX_IMAGE_HEIGHT |
27 | #define JP2_MAX_IMAGE_HEIGHT JP2_MAX_IMAGE_WIDTH |
28 | #endif |
29 | |
30 | /* *** JP2_MAX_IMAGE_PIXELS *** |
31 | * OpenJPEG seems limited to an image of 2 gigapixel size. |
32 | */ |
33 | #ifndef JP2_MAX_IMAGE_PIXELS |
34 | #define JP2_MAX_IMAGE_PIXELS std::numeric_limits<qint32>::max() |
35 | #endif |
36 | |
37 | /* *** JP2_ENABLE_HDR *** |
38 | * Enable float image formats. Disabled by default |
39 | * due to lack of test images. |
40 | */ |
41 | #ifndef JP2_ENABLE_HDR |
42 | // #define JP2_ENABLE_HDR |
43 | #endif |
44 | |
45 | #define JP2_SUBTYPE QByteArrayLiteral("JP2") |
46 | #define J2K_SUBTYPE QByteArrayLiteral("J2K") |
47 | |
48 | static void error_callback(const char *msg, void *client_data) |
49 | { |
50 | Q_UNUSED(client_data) |
51 | qCritical() << msg; |
52 | } |
53 | |
54 | static void warning_callback(const char *msg, void *client_data) |
55 | { |
56 | Q_UNUSED(client_data) |
57 | qWarning() << msg; |
58 | } |
59 | |
60 | static void info_callback(const char *msg, void *client_data) |
61 | { |
62 | Q_UNUSED(client_data) |
63 | qInfo() << msg; |
64 | } |
65 | |
66 | static OPJ_SIZE_T jp2_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) |
67 | { |
68 | auto dev = (QIODevice*)p_user_data; |
69 | if (dev == nullptr) { |
70 | return OPJ_SIZE_T(-1); |
71 | } |
72 | return OPJ_SIZE_T(dev->read(data: (char*)p_buffer, maxlen: (qint64)p_nb_bytes)); |
73 | } |
74 | |
75 | static OPJ_SIZE_T jp2_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) |
76 | { |
77 | auto dev = (QIODevice*)p_user_data; |
78 | if (dev == nullptr) { |
79 | return OPJ_SIZE_T(-1); |
80 | } |
81 | return OPJ_SIZE_T(dev->write(data: (char*)p_buffer, len: (qint64)p_nb_bytes)); |
82 | } |
83 | |
84 | static OPJ_BOOL jp2_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) |
85 | { |
86 | auto dev = (QIODevice*)p_user_data; |
87 | if (dev == nullptr) { |
88 | return OPJ_FALSE; |
89 | } |
90 | return dev->seek(pos: p_nb_bytes) ? OPJ_TRUE : OPJ_FALSE; |
91 | } |
92 | |
93 | static OPJ_OFF_T jp2_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) |
94 | { |
95 | auto dev = (QIODevice*)p_user_data; |
96 | if (dev == nullptr) { |
97 | return OPJ_OFF_T(); |
98 | } |
99 | if (dev->seek(pos: dev->pos() + p_nb_bytes)) { |
100 | return p_nb_bytes; |
101 | } |
102 | return OPJ_OFF_T(); |
103 | } |
104 | |
105 | class JP2HandlerPrivate |
106 | { |
107 | public: |
108 | JP2HandlerPrivate() |
109 | : m_jp2_stream(nullptr) |
110 | , m_jp2_image(nullptr) |
111 | , m_jp2_version(0) |
112 | , m_jp2_codec(nullptr) |
113 | , m_quality(-1) |
114 | , m_subtype(JP2_SUBTYPE) |
115 | { |
116 | auto sver = QString::fromLatin1(ba: QByteArray(opj_version())).split(sep: QChar(u'.')); |
117 | if (sver.size() == 3) { |
118 | bool ok1, ok2, ok3; |
119 | auto v1 = sver.at(i: 0).toInt(ok: &ok1); |
120 | auto v2 = sver.at(i: 1).toInt(ok: &ok2); |
121 | auto v3 = sver.at(i: 2).toInt(ok: &ok3); |
122 | if (ok1 && ok2 && ok3) |
123 | m_jp2_version = QT_VERSION_CHECK(v1, v2, v3); |
124 | } |
125 | } |
126 | ~JP2HandlerPrivate() |
127 | { |
128 | if (m_jp2_image) { |
129 | opj_image_destroy(image: m_jp2_image); |
130 | m_jp2_image = nullptr; |
131 | } |
132 | if (m_jp2_stream) { |
133 | opj_stream_destroy(p_stream: m_jp2_stream); |
134 | m_jp2_stream = nullptr; |
135 | } |
136 | if (m_jp2_codec) { |
137 | opj_destroy_codec(p_codec: m_jp2_codec); |
138 | m_jp2_codec = nullptr; |
139 | } |
140 | } |
141 | |
142 | /*! |
143 | * \brief detectDecoderFormat |
144 | * \param device |
145 | * \return The codec JP2 found. |
146 | */ |
147 | OPJ_CODEC_FORMAT detectDecoderFormat(QIODevice *device) const |
148 | { |
149 | auto ba = device->peek(maxlen: 32); |
150 | if (ba.left(n: 12) == QByteArray::fromHex(hexEncoded: "0000000c6a5020200d0a870a" )) { |
151 | // if (ba.mid(20, 4) == QByteArray::fromHex("6a707820")) // 'jpx ' |
152 | // return OPJ_CODEC_JPX; // JPEG 2000 Part 2 (not supported -> try reading as JP2) |
153 | return OPJ_CODEC_JP2; |
154 | } |
155 | if (ba.left(n: 5) == QByteArray::fromHex(hexEncoded: "ff4fff5100" )) { |
156 | return OPJ_CODEC_J2K; |
157 | } |
158 | return OPJ_CODEC_UNKNOWN; |
159 | } |
160 | |
161 | bool createStream(QIODevice *device, bool read) |
162 | { |
163 | if (device == nullptr) { |
164 | return false; |
165 | } |
166 | if (m_jp2_stream == nullptr) { |
167 | m_jp2_stream = opj_stream_default_create(p_is_input: read ? OPJ_TRUE : OPJ_FALSE); |
168 | } |
169 | if (m_jp2_stream == nullptr) { |
170 | return false; |
171 | } |
172 | opj_stream_set_user_data(p_stream: m_jp2_stream, p_data: device, p_function: nullptr); |
173 | opj_stream_set_user_data_length(p_stream: m_jp2_stream, data_length: read ? device->size() : 0); |
174 | opj_stream_set_read_function(p_stream: m_jp2_stream, p_function: jp2_read); |
175 | opj_stream_set_write_function(p_stream: m_jp2_stream, p_function: jp2_write); |
176 | opj_stream_set_skip_function(p_stream: m_jp2_stream, p_function: jp2_skip); |
177 | opj_stream_set_seek_function(p_stream: m_jp2_stream, p_function: jp2_seek); |
178 | return true; |
179 | } |
180 | |
181 | bool isImageValid(const opj_image_t *i) const |
182 | { |
183 | return i && i->comps && i->numcomps > 0; |
184 | } |
185 | |
186 | void enableThreads(opj_codec_t *codec) const |
187 | { |
188 | if (!opj_has_thread_support()) { |
189 | qInfo() << "OpenJPEG doesn't support multi-threading!" ; |
190 | } else if (!opj_codec_set_threads(p_codec: codec, num_threads: std::max(a: 1, b: QThread::idealThreadCount() / 2))) { |
191 | qWarning() << "Unable to enable multi-threading!" ; |
192 | } |
193 | } |
194 | |
195 | bool createDecoder(QIODevice *device) |
196 | { |
197 | if (m_jp2_codec) { |
198 | return true; |
199 | } |
200 | auto jp2Format = detectDecoderFormat(device); |
201 | if (jp2Format == OPJ_CODEC_UNKNOWN) { |
202 | return false; |
203 | } |
204 | m_jp2_codec = opj_create_decompress(format: jp2Format); |
205 | if (m_jp2_codec == nullptr) { |
206 | return false; |
207 | } |
208 | enableThreads(codec: m_jp2_codec); |
209 | #ifdef QT_DEBUG |
210 | // opj_set_info_handler(m_jp2_codec, info_callback, nullptr); |
211 | // opj_set_warning_handler(m_jp2_codec, warning_callback, nullptr); |
212 | #endif |
213 | opj_set_error_handler(p_codec: m_jp2_codec, p_callback: error_callback, p_user_data: nullptr); |
214 | return true; |
215 | } |
216 | |
217 | bool readHeader(QIODevice *device) |
218 | { |
219 | if (!createStream(device, read: true)) { |
220 | return false; |
221 | } |
222 | |
223 | if (m_jp2_image) { |
224 | return true; |
225 | } |
226 | |
227 | if (!createDecoder(device)) { |
228 | return false; |
229 | } |
230 | |
231 | opj_set_default_decoder_parameters(parameters: &m_dparameters); |
232 | if (!opj_setup_decoder(p_codec: m_jp2_codec, parameters: &m_dparameters)) { |
233 | qCritical() << "Failed to setup JP2 decoder!" ; |
234 | return false; |
235 | } |
236 | |
237 | if (!opj_read_header(p_stream: m_jp2_stream, p_codec: m_jp2_codec, p_image: &m_jp2_image)) { |
238 | qCritical() << "Failed to read JP2 header!" ; |
239 | return false; |
240 | } |
241 | |
242 | return isImageValid(i: m_jp2_image); |
243 | } |
244 | |
245 | template<class T> |
246 | bool jp2ToImage(QImage *img) const |
247 | { |
248 | Q_ASSERT(img->depth() == 8 * sizeof(T) || img->depth() == 32 * sizeof(T)); |
249 | for (qint32 c = 0, cc = m_jp2_image->numcomps; c < cc; ++c) { |
250 | auto cs = cc == 1 ? 1 : 4; |
251 | auto &&jc = m_jp2_image->comps[c]; |
252 | if (jc.data == nullptr) |
253 | return false; |
254 | if (qint32(jc.w) != img->width() || qint32(jc.h) != img->height()) |
255 | return false; |
256 | |
257 | // discriminate between int and float (avoid complicating things by creating classes with template specializations) |
258 | if (std::numeric_limits<T>::is_integer) { |
259 | auto divisor = 1; |
260 | if (jc.prec > sizeof(T) * 8) { |
261 | // convert to the wanted precision (e.g. 16-bit -> 8-bit: divisor = 65535 / 255 = 257) |
262 | divisor = std::max(a: 1, b: int(((1ll << jc.prec) - 1) / ((1ll << (sizeof(T) * 8)) - 1))); |
263 | } |
264 | for (qint32 y = 0, h = img->height(); y < h; ++y) { |
265 | auto ptr = reinterpret_cast<T *>(img->scanLine(y)); |
266 | for (qint32 x = 0, w = img->width(); x < w; ++x) { |
267 | qint32 v = jc.data[y * w + x] / divisor; |
268 | if (jc.sgnd) // never seen |
269 | v -= std::numeric_limits<typename std::make_signed<T>::type>::min(); |
270 | *(ptr + x * cs + c) = std::clamp(val: v, lo: qint32(std::numeric_limits<T>::lowest()), hi: qint32(std::numeric_limits<T>::max())); |
271 | } |
272 | } |
273 | } else { // float |
274 | for (qint32 y = 0, h = img->height(); y < h; ++y) { |
275 | auto ptr = reinterpret_cast<T *>(img->scanLine(y)); |
276 | for (qint32 x = 0, w = img->width(); x < w; ++x) { |
277 | *(ptr + x * cs + c) = jc.data[y * w + x]; |
278 | } |
279 | } |
280 | } |
281 | } |
282 | return true; |
283 | } |
284 | |
285 | template<class T> |
286 | void alphaFix(QImage *img) const |
287 | { |
288 | if (m_jp2_image->numcomps == 3) { |
289 | Q_ASSERT(img->depth() == 32 * sizeof(T)); |
290 | for (qint32 y = 0, h = img->height(); y < h; ++y) { |
291 | auto ptr = reinterpret_cast<T *>(img->scanLine(y)); |
292 | for (qint32 x = 0, w = img->width(); x < w; ++x) { |
293 | *(ptr + x * 4 + 3) = std::numeric_limits<T>::is_iec559 ? 1 : std::numeric_limits<T>::max(); |
294 | } |
295 | } |
296 | } |
297 | } |
298 | |
299 | QImage readImage(QIODevice *device) |
300 | { |
301 | if (!readHeader(device)) { |
302 | return {}; |
303 | } |
304 | |
305 | auto img = imageAlloc(size: size(), format: format()); |
306 | if (img.isNull()) { |
307 | return {}; |
308 | } |
309 | |
310 | if (!opj_decode(p_decompressor: m_jp2_codec, p_stream: m_jp2_stream, p_image: m_jp2_image)) { |
311 | qCritical() << "Failed to decoding JP2 image!" ; |
312 | return {}; |
313 | } |
314 | |
315 | auto f = img.format(); |
316 | if (f == QImage::Format_RGBA32FPx4 || f == QImage::Format_RGBX32FPx4) { |
317 | if (!jp2ToImage<quint32>(img: &img)) |
318 | return {}; |
319 | alphaFix<float>(img: &img); |
320 | } else if (f == QImage::Format_RGBA64 || f == QImage::Format_RGBX64 || f == QImage::Format_Grayscale16) { |
321 | if (!jp2ToImage<quint16>(img: &img)) |
322 | return {}; |
323 | alphaFix<quint16>(img: &img); |
324 | } else { |
325 | if (!jp2ToImage<quint8>(img: &img)) |
326 | return {}; |
327 | alphaFix<quint8>(img: &img); |
328 | } |
329 | |
330 | img.setColorSpace(colorSpace()); |
331 | |
332 | return img; |
333 | } |
334 | |
335 | bool checkSizeLimits(qint32 width, qint32 height, qint32 nchannels) const |
336 | { |
337 | if (width > JP2_MAX_IMAGE_WIDTH || height > JP2_MAX_IMAGE_HEIGHT || width < 1 || height < 1) { |
338 | qCritical() << "Maximum image size is limited to" << JP2_MAX_IMAGE_WIDTH << "x" << JP2_MAX_IMAGE_HEIGHT << "pixels" ; |
339 | return false; |
340 | } |
341 | |
342 | if (qint64(width) * qint64(height) > JP2_MAX_IMAGE_PIXELS) { |
343 | qCritical() << "Maximum image size is limited to" << JP2_MAX_IMAGE_PIXELS << "pixels" ; |
344 | return false; |
345 | } |
346 | |
347 | // OpenJPEG uses a shadow copy @32-bit/channel so we need to do a check |
348 | auto maxBytes = qint64(QImageReader::allocationLimit()) * 1024 * 1024; |
349 | auto neededBytes = qint64(width) * height * nchannels * 4; |
350 | if (maxBytes > 0 && neededBytes > maxBytes) { |
351 | qCritical() << "Allocation limit set to" << (maxBytes / 1024 / 1024) << "MiB but" << (neededBytes / 1024 / 1024) << "MiB are needed!" ; |
352 | return false; |
353 | } |
354 | |
355 | return true; |
356 | } |
357 | |
358 | bool checkSizeLimits(const QSize &size, qint32 nchannels) const |
359 | { |
360 | return checkSizeLimits(width: size.width(), height: size.height(), nchannels); |
361 | } |
362 | |
363 | QSize size() const |
364 | { |
365 | QSize sz; |
366 | if (isImageValid(i: m_jp2_image)) { |
367 | auto &&c0 = m_jp2_image->comps[0]; |
368 | auto tmp = QSize(c0.w, c0.h); |
369 | if (checkSizeLimits(size: tmp, nchannels: m_jp2_image->numcomps)) |
370 | sz = tmp; |
371 | } |
372 | return sz; |
373 | } |
374 | |
375 | QImage::Format format() const |
376 | { |
377 | auto fmt = QImage::Format_Invalid; |
378 | if (isImageValid(i: m_jp2_image)) { |
379 | auto &&c0 = m_jp2_image->comps[0]; |
380 | auto prec = c0.prec; |
381 | for (quint32 c = 1; c < m_jp2_image->numcomps; ++c) { |
382 | auto &&cc = m_jp2_image->comps[c]; |
383 | if (cc.prec != prec) |
384 | prec = 0; |
385 | } |
386 | auto jp2cs = m_jp2_image->color_space; |
387 | #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
388 | if (jp2cs == OPJ_CLRSPC_UNKNOWN || jp2cs == OPJ_CLRSPC_UNSPECIFIED) { |
389 | auto cs = colorSpace(); |
390 | if (cs.colorModel() == QColorSpace::ColorModel::Cmyk) |
391 | jp2cs = OPJ_CLRSPC_CMYK; |
392 | else if (cs.colorModel() == QColorSpace::ColorModel::Rgb) |
393 | jp2cs = OPJ_CLRSPC_SRGB; |
394 | else if (cs.colorModel() == QColorSpace::ColorModel::Gray) |
395 | jp2cs = OPJ_CLRSPC_GRAY; |
396 | } |
397 | #endif |
398 | if (jp2cs == OPJ_CLRSPC_UNKNOWN || jp2cs == OPJ_CLRSPC_UNSPECIFIED) { |
399 | if (m_jp2_image->numcomps == 1) |
400 | jp2cs = OPJ_CLRSPC_GRAY; |
401 | else |
402 | jp2cs = OPJ_CLRSPC_SRGB; |
403 | } |
404 | |
405 | // *** IMPORTANT: To keep the code simple, the returned formats must have 1 or 4 channels (8/16/32-bits) |
406 | if (jp2cs == OPJ_CLRSPC_SRGB) { |
407 | if (m_jp2_image->numcomps == 3 || m_jp2_image->numcomps == 4) { |
408 | auto hasAlpha = m_jp2_image->numcomps == 4; |
409 | if (prec == 8) |
410 | fmt = hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGBX8888; |
411 | else if (prec == 16) |
412 | fmt = hasAlpha ? QImage::Format_RGBA64 : QImage::Format_RGBX64; |
413 | #ifdef JP2_ENABLE_HDR |
414 | else if (prec == 32) // not sure about this |
415 | fmt = hasAlpha ? QImage::Format_RGBA32FPx4 : QImage::Format_RGBX32FPx4; |
416 | #endif |
417 | } |
418 | } else if (jp2cs == OPJ_CLRSPC_GRAY) { |
419 | if (m_jp2_image->numcomps == 1) { |
420 | if (prec == 8) |
421 | fmt = QImage::Format_Grayscale8; |
422 | else if (prec == 16) |
423 | fmt = QImage::Format_Grayscale16; |
424 | } |
425 | } else if (jp2cs == OPJ_CLRSPC_CMYK) { |
426 | if (m_jp2_image->numcomps == 4) { |
427 | #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
428 | if (prec == 8 || prec == 16) |
429 | fmt = QImage::Format_CMYK8888; |
430 | #endif |
431 | } |
432 | } |
433 | } |
434 | return fmt; |
435 | } |
436 | |
437 | QColorSpace colorSpace() const |
438 | { |
439 | QColorSpace cs; |
440 | if (m_jp2_image) { |
441 | if (m_jp2_image->icc_profile_buf && m_jp2_image->icc_profile_len > 0) { |
442 | cs = QColorSpace::fromIccProfile(iccProfile: QByteArray((char *)m_jp2_image->icc_profile_buf, m_jp2_image->icc_profile_len)); |
443 | } |
444 | if (!cs.isValid()) { |
445 | if (m_jp2_image->color_space == OPJ_CLRSPC_SRGB) |
446 | cs = QColorSpace(QColorSpace::SRgb); |
447 | } |
448 | } |
449 | return cs; |
450 | } |
451 | |
452 | /*! |
453 | * \brief isSupported |
454 | * \return True if the current JP2 image i ssupported by the plugin. Otherwise false. |
455 | */ |
456 | bool isSupported() const |
457 | { |
458 | return format() != QImage::Format_Invalid; |
459 | } |
460 | |
461 | QByteArray subType() const |
462 | { |
463 | return m_subtype; |
464 | } |
465 | void setSubType(const QByteArray &type) |
466 | { |
467 | m_subtype = type; |
468 | } |
469 | |
470 | qint32 quality() const |
471 | { |
472 | return m_quality; |
473 | } |
474 | void setQuality(qint32 quality) |
475 | { |
476 | m_quality = std::clamp(val: quality, lo: -1, hi: 100); |
477 | } |
478 | |
479 | /*! |
480 | * \brief encoderFormat |
481 | * \return The encoder format set by subType. |
482 | */ |
483 | OPJ_CODEC_FORMAT encoderFormat() const |
484 | { |
485 | return subType() == J2K_SUBTYPE ? OPJ_CODEC_J2K : OPJ_CODEC_JP2; |
486 | } |
487 | |
488 | /*! |
489 | * \brief opjVersion |
490 | * \return The runtime library version. |
491 | */ |
492 | qint32 opjVersion() const |
493 | { |
494 | return m_jp2_version; |
495 | } |
496 | |
497 | bool imageToJp2(const QImage &image) |
498 | { |
499 | auto ncomp = image.hasAlphaChannel() ? 4 : 3; |
500 | auto prec = 8; |
501 | auto convFormat = image.format(); |
502 | auto isFloat = false; |
503 | auto cs = OPJ_CLRSPC_SRGB; |
504 | if (opjVersion() >= QT_VERSION_CHECK(2, 5, 4)) { |
505 | auto ics = image.colorSpace(); |
506 | if (!(ics.isValid() && ics.primaries() == QColorSpace::Primaries::SRgb && ics.transferFunction() == QColorSpace::TransferFunction::SRgb)) { |
507 | cs = OPJ_CLRSPC_UNKNOWN; |
508 | } |
509 | } |
510 | |
511 | switch (image.format()) { |
512 | case QImage::Format_Mono: |
513 | case QImage::Format_MonoLSB: |
514 | case QImage::Format_Alpha8: |
515 | case QImage::Format_Grayscale8: |
516 | ncomp = 1; |
517 | cs = OPJ_CLRSPC_GRAY; |
518 | convFormat = QImage::Format_Grayscale8; |
519 | break; |
520 | case QImage::Format_Indexed8: |
521 | if (image.isGrayscale()) { |
522 | ncomp = 1; |
523 | cs = OPJ_CLRSPC_GRAY; |
524 | convFormat = QImage::Format_Grayscale8; |
525 | } else { |
526 | ncomp = 4; |
527 | cs = OPJ_CLRSPC_SRGB; |
528 | convFormat = QImage::Format_RGBA8888; |
529 | } |
530 | break; |
531 | case QImage::Format_Grayscale16: |
532 | ncomp = 1; |
533 | prec = 16; |
534 | cs = OPJ_CLRSPC_GRAY; |
535 | convFormat = QImage::Format_Grayscale16; |
536 | break; |
537 | case QImage::Format_RGBX16FPx4: |
538 | case QImage::Format_RGBX32FPx4: |
539 | isFloat = true; |
540 | #ifdef JP2_ENABLE_HDR |
541 | prec = 32; |
542 | convFormat = QImage::Format_RGBX32FPx4; |
543 | cs = OPJ_CLRSPC_UNSPECIFIED; |
544 | break; |
545 | #else |
546 | Q_FALLTHROUGH(); |
547 | #endif |
548 | case QImage::Format_RGBX64: |
549 | case QImage::Format_RGB30: |
550 | case QImage::Format_BGR30: |
551 | prec = 16; |
552 | convFormat = QImage::Format_RGBX64; |
553 | break; |
554 | |
555 | case QImage::Format_RGBA16FPx4: |
556 | case QImage::Format_RGBA16FPx4_Premultiplied: |
557 | case QImage::Format_RGBA32FPx4: |
558 | case QImage::Format_RGBA32FPx4_Premultiplied: |
559 | isFloat = true; |
560 | #ifdef JP2_ENABLE_HDR |
561 | prec = 32; |
562 | convFormat = QImage::Format_RGBA32FPx4; |
563 | cs = OPJ_CLRSPC_UNSPECIFIED; |
564 | break; |
565 | #else |
566 | Q_FALLTHROUGH(); |
567 | #endif |
568 | case QImage::Format_RGBA64: |
569 | case QImage::Format_RGBA64_Premultiplied: |
570 | case QImage::Format_A2RGB30_Premultiplied: |
571 | case QImage::Format_A2BGR30_Premultiplied: |
572 | prec = 16; |
573 | convFormat = QImage::Format_RGBA64; |
574 | break; |
575 | #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
576 | case QImage::Format_CMYK8888: // requires OpenJPEG 2.5.3+ |
577 | if (opjVersion() >= QT_VERSION_CHECK(2, 5, 3)) { |
578 | ncomp = 4; |
579 | cs = OPJ_CLRSPC_CMYK; |
580 | break; |
581 | } else { |
582 | Q_FALLTHROUGH(); |
583 | } |
584 | #endif |
585 | default: |
586 | if (image.depth() > 32) { |
587 | qWarning() << "The image is saved losing precision!" ; |
588 | } |
589 | convFormat = ncomp == 4 ? QImage::Format_RGBA8888 : QImage::Format_RGBX8888; |
590 | break; |
591 | } |
592 | |
593 | if (!checkSizeLimits(size: image.size(), nchannels: ncomp)) { |
594 | return false; |
595 | } |
596 | |
597 | opj_set_default_encoder_parameters(parameters: &m_cparameters); |
598 | m_cparameters.cod_format = encoderFormat(); |
599 | m_cparameters.tile_size_on = 1; |
600 | m_cparameters.cp_tdx = 1024; |
601 | m_cparameters.cp_tdy = 1024; |
602 | |
603 | if (m_quality > -1 && m_quality < 100) { |
604 | m_cparameters.irreversible = 1; |
605 | m_cparameters.tcp_numlayers = 1; |
606 | m_cparameters.cp_disto_alloc = 1; |
607 | m_cparameters.tcp_rates[0] = 100.0 - (m_quality < 10 ? m_quality : 10 + (std::log10(x: m_quality) - 1) * 90); |
608 | } |
609 | |
610 | std::unique_ptr<opj_image_cmptparm_t> cmptparm(new opj_image_cmptparm_t[ncomp]); |
611 | for (int i = 0; i < ncomp; ++i) { |
612 | auto &&p = cmptparm.get() + i; |
613 | memset(s: p, c: 0, n: sizeof(opj_image_cmptparm_t)); |
614 | p->dx = m_cparameters.subsampling_dx; |
615 | p->dy = m_cparameters.subsampling_dy; |
616 | p->w = image.width(); |
617 | p->h = image.height(); |
618 | p->x0 = 0; |
619 | p->y0 = 0; |
620 | p->prec = prec; |
621 | p->sgnd = 0; |
622 | } |
623 | |
624 | m_jp2_image = opj_image_create(numcmpts: ncomp, cmptparms: cmptparm.get(), clrspc: cs); |
625 | if (m_jp2_image == nullptr) { |
626 | return false; |
627 | } |
628 | if (int(m_jp2_image->numcomps) != ncomp) { |
629 | return false; // paranoia |
630 | } |
631 | m_jp2_image->x1 = image.width(); |
632 | m_jp2_image->y1 = image.height(); |
633 | |
634 | ScanLineConverter scl(convFormat); |
635 | if (prec < 32 && isFloat) { |
636 | scl.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgbLinear)); |
637 | } |
638 | if (cs == OPJ_CLRSPC_SRGB) { |
639 | scl.setTargetColorSpace(QColorSpace(QColorSpace::SRgb)); |
640 | } else { |
641 | scl.setTargetColorSpace(image.colorSpace()); |
642 | } |
643 | for (qint32 c = 0; c < ncomp; ++c) { |
644 | auto &&comp = m_jp2_image->comps[c]; |
645 | auto mul = ncomp == 1 ? 1 : 4; |
646 | for (qint32 y = 0, h = image.height(); y < h; ++y) { |
647 | if (prec == 8) { |
648 | auto ptr = reinterpret_cast<const quint8 *>(scl.convertedScanLine(image, y)); |
649 | for (qint32 x = 0, w = image.width(); x < w; ++x) |
650 | comp.data[y * w + x] = ptr[x * mul + c]; |
651 | } else if (prec == 16) { |
652 | auto ptr = reinterpret_cast<const quint16 *>(scl.convertedScanLine(image, y)); |
653 | for (qint32 x = 0, w = image.width(); x < w; ++x) |
654 | comp.data[y * w + x] = ptr[x * mul + c]; |
655 | } else if (prec == 32) { |
656 | auto ptr = reinterpret_cast<const quint32 *>(scl.convertedScanLine(image, y)); |
657 | for (qint32 x = 0, w = image.width(); x < w; ++x) |
658 | comp.data[y * w + x] = ptr[x * mul + c]; |
659 | } |
660 | } |
661 | } |
662 | |
663 | if (opjVersion() >= QT_VERSION_CHECK(2, 5, 4)) { |
664 | auto colorSpace = scl.targetColorSpace().iccProfile(); |
665 | if (!colorSpace.isEmpty()) { |
666 | m_jp2_image->icc_profile_buf = reinterpret_cast<OPJ_BYTE *>(malloc(size: colorSpace.size())); |
667 | if (m_jp2_image->icc_profile_buf) { |
668 | memcpy(dest: m_jp2_image->icc_profile_buf, src: colorSpace.data(), n: colorSpace.size()); |
669 | m_jp2_image->icc_profile_len = colorSpace.size(); |
670 | } |
671 | } |
672 | } |
673 | |
674 | return true; |
675 | } |
676 | |
677 | bool writeImage(QIODevice *device, const QImage &image) |
678 | { |
679 | if (!imageToJp2(image)) { |
680 | qCritical() << "Error while creating JP2 image!" ; |
681 | return false; |
682 | } |
683 | |
684 | std::unique_ptr<opj_codec_t, std::function<void(opj_codec_t *)>> codec(opj_create_compress(format: encoderFormat()), opj_destroy_codec); |
685 | if (codec == nullptr) { |
686 | qCritical() << "Error while creating encoder!" ; |
687 | return false; |
688 | } |
689 | enableThreads(codec: codec.get()); |
690 | #ifdef QT_DEBUG |
691 | // opj_set_info_handler(m_jp2_codec, info_callback, nullptr); |
692 | // opj_set_warning_handler(m_jp2_codec, warning_callback, nullptr); |
693 | #endif |
694 | opj_set_error_handler(p_codec: m_jp2_codec, p_callback: error_callback, p_user_data: nullptr); |
695 | |
696 | if (!opj_setup_encoder(p_codec: codec.get(), parameters: &m_cparameters, image: m_jp2_image)) { |
697 | return false; |
698 | } |
699 | |
700 | if (!createStream(device, read: false)) { |
701 | return false; |
702 | } |
703 | |
704 | if (!opj_start_compress(p_codec: codec.get(), p_image: m_jp2_image, p_stream: m_jp2_stream)) { |
705 | return false; |
706 | } |
707 | if (!opj_encode(p_codec: codec.get(), p_stream: m_jp2_stream)) { |
708 | return false; |
709 | } |
710 | if (!opj_end_compress(p_codec: codec.get(), p_stream: m_jp2_stream)) { |
711 | return false; |
712 | } |
713 | |
714 | return true; |
715 | } |
716 | |
717 | private: |
718 | // common |
719 | opj_stream_t *m_jp2_stream; |
720 | |
721 | opj_image_t *m_jp2_image; |
722 | |
723 | qint32 m_jp2_version; |
724 | |
725 | // read |
726 | opj_codec_t *m_jp2_codec; |
727 | |
728 | opj_dparameters_t m_dparameters; |
729 | |
730 | // write |
731 | opj_cparameters_t m_cparameters; |
732 | |
733 | qint32 m_quality; |
734 | |
735 | QByteArray m_subtype; |
736 | }; |
737 | |
738 | |
739 | JP2Handler::JP2Handler() |
740 | : QImageIOHandler() |
741 | , d(new JP2HandlerPrivate) |
742 | { |
743 | } |
744 | |
745 | bool JP2Handler::canRead() const |
746 | { |
747 | if (canRead(device: device())) { |
748 | setFormat("jp2" ); |
749 | return true; |
750 | } |
751 | return false; |
752 | } |
753 | |
754 | bool JP2Handler::canRead(QIODevice *device) |
755 | { |
756 | if (!device) { |
757 | qWarning(msg: "JP2Handler::canRead() called with no device" ); |
758 | return false; |
759 | } |
760 | |
761 | if (device->isSequential()) { |
762 | return false; |
763 | } |
764 | |
765 | JP2HandlerPrivate handler; |
766 | return handler.detectDecoderFormat(device) != OPJ_CODEC_UNKNOWN; |
767 | } |
768 | |
769 | bool JP2Handler::read(QImage *image) |
770 | { |
771 | auto dev = device(); |
772 | if (dev == nullptr) { |
773 | return false; |
774 | } |
775 | auto img = d->readImage(device: dev); |
776 | if (img.isNull()) { |
777 | return false; |
778 | } |
779 | *image = img; |
780 | return true; |
781 | } |
782 | |
783 | bool JP2Handler::write(const QImage &image) |
784 | { |
785 | if (image.isNull()) { |
786 | return false; |
787 | } |
788 | auto dev = device(); |
789 | if (dev == nullptr) { |
790 | return false; |
791 | } |
792 | return d->writeImage(device: dev, image); |
793 | } |
794 | |
795 | bool JP2Handler::supportsOption(ImageOption option) const |
796 | { |
797 | if (option == QImageIOHandler::Size) { |
798 | return true; |
799 | } |
800 | if (option == QImageIOHandler::ImageFormat) { |
801 | return true; |
802 | } |
803 | if (option == QImageIOHandler::SubType) { |
804 | return true; |
805 | } |
806 | if (option == QImageIOHandler::SupportedSubTypes) { |
807 | return true; |
808 | } |
809 | if (option == QImageIOHandler::Quality) { |
810 | return true; |
811 | } |
812 | return false; |
813 | } |
814 | |
815 | void JP2Handler::setOption(ImageOption option, const QVariant &value) |
816 | { |
817 | if (option == QImageIOHandler::SubType) { |
818 | auto st = value.toByteArray(); |
819 | if (this->option(option: QImageIOHandler::SupportedSubTypes).toList().contains(t: st)) |
820 | d->setSubType(st); |
821 | } |
822 | if (option == QImageIOHandler::Quality) { |
823 | auto ok = false; |
824 | auto q = value.toInt(ok: &ok); |
825 | if (ok) { |
826 | d->setQuality(q); |
827 | } |
828 | } |
829 | } |
830 | |
831 | QVariant JP2Handler::option(ImageOption option) const |
832 | { |
833 | QVariant v; |
834 | |
835 | if (option == QImageIOHandler::Size) { |
836 | if (d->readHeader(device: device())) { |
837 | v = d->size(); |
838 | } |
839 | } |
840 | |
841 | if (option == QImageIOHandler::ImageFormat) { |
842 | if (d->readHeader(device: device())) { |
843 | v = d->format(); |
844 | } |
845 | } |
846 | |
847 | if (option == QImageIOHandler::SubType) { |
848 | v = d->subType(); |
849 | } |
850 | |
851 | if (option == QImageIOHandler::SupportedSubTypes) { |
852 | v = QVariant::fromValue(value: QList<QByteArray>() << JP2_SUBTYPE << J2K_SUBTYPE); |
853 | } |
854 | |
855 | if (option == QImageIOHandler::Quality) { |
856 | v = d->quality(); |
857 | } |
858 | |
859 | return v; |
860 | } |
861 | |
862 | QImageIOPlugin::Capabilities JP2Plugin::capabilities(QIODevice *device, const QByteArray &format) const |
863 | { |
864 | if (format == "jp2" || format == "j2k" ) { |
865 | return Capabilities(CanRead | CanWrite); |
866 | } |
867 | // NOTE: JPF is the default extension of Photoshop for JP2 files. |
868 | if (format == "jpf" ) { |
869 | return Capabilities(CanRead); |
870 | } |
871 | if (!format.isEmpty()) { |
872 | return {}; |
873 | } |
874 | if (!device->isOpen()) { |
875 | return {}; |
876 | } |
877 | |
878 | Capabilities cap; |
879 | if (device->isReadable() && JP2Handler::canRead(device)) { |
880 | cap |= CanRead; |
881 | } |
882 | if (device->isWritable()) { |
883 | cap |= CanWrite; |
884 | } |
885 | return cap; |
886 | } |
887 | |
888 | QImageIOHandler *JP2Plugin::create(QIODevice *device, const QByteArray &format) const |
889 | { |
890 | QImageIOHandler *handler = new JP2Handler; |
891 | handler->setDevice(device); |
892 | handler->setFormat(format); |
893 | return handler; |
894 | } |
895 | |
896 | #include "moc_jp2_p.cpp" |
897 | |