/* This file is part of the KDE project SPDX-FileCopyrightText: 2024 Mirco Miranda SPDX-License-Identifier: LGPL-2.1-or-later */ #include "jp2_p.h" #include "scanlineconverter_p.h" #include "util_p.h" #include #include #include #include #include #include /* *** JP2_MAX_IMAGE_WIDTH and JP2_MAX_IMAGE_HEIGHT *** * The maximum size in pixel allowed by the plugin. */ #ifndef JP2_MAX_IMAGE_WIDTH #define JP2_MAX_IMAGE_WIDTH 300000 #endif #ifndef JP2_MAX_IMAGE_HEIGHT #define JP2_MAX_IMAGE_HEIGHT JP2_MAX_IMAGE_WIDTH #endif /* *** JP2_MAX_IMAGE_PIXELS *** * OpenJPEG seems limited to an image of 2 gigapixel size. */ #ifndef JP2_MAX_IMAGE_PIXELS #define JP2_MAX_IMAGE_PIXELS std::numeric_limits::max() #endif /* *** JP2_ENABLE_HDR *** * Enable float image formats. Disabled by default * due to lack of test images. */ #ifndef JP2_ENABLE_HDR // #define JP2_ENABLE_HDR #endif #define JP2_SUBTYPE QByteArrayLiteral("JP2") #define J2K_SUBTYPE QByteArrayLiteral("J2K") static void error_callback(const char *msg, void *client_data) { Q_UNUSED(client_data) qCritical() << msg; } static void warning_callback(const char *msg, void *client_data) { Q_UNUSED(client_data) qWarning() << msg; } static void info_callback(const char *msg, void *client_data) { Q_UNUSED(client_data) qInfo() << msg; } static OPJ_SIZE_T jp2_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { auto dev = (QIODevice*)p_user_data; if (dev == nullptr) { return OPJ_SIZE_T(-1); } return OPJ_SIZE_T(dev->read((char*)p_buffer, (qint64)p_nb_bytes)); } static OPJ_SIZE_T jp2_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { auto dev = (QIODevice*)p_user_data; if (dev == nullptr) { return OPJ_SIZE_T(-1); } return OPJ_SIZE_T(dev->write((char*)p_buffer, (qint64)p_nb_bytes)); } static OPJ_BOOL jp2_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) { auto dev = (QIODevice*)p_user_data; if (dev == nullptr) { return OPJ_FALSE; } return dev->seek(p_nb_bytes) ? OPJ_TRUE : OPJ_FALSE; } static OPJ_OFF_T jp2_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { auto dev = (QIODevice*)p_user_data; if (dev == nullptr) { return OPJ_OFF_T(); } if (dev->seek(dev->pos() + p_nb_bytes)) { return p_nb_bytes; } return OPJ_OFF_T(); } class JP2HandlerPrivate { public: JP2HandlerPrivate() : m_jp2_stream(nullptr) , m_jp2_image(nullptr) , m_jp2_version(0) , m_jp2_codec(nullptr) , m_quality(-1) , m_subtype(JP2_SUBTYPE) { auto sver = QString::fromLatin1(QByteArray(opj_version())).split(QChar(u'.')); if (sver.size() == 3) { bool ok1, ok2, ok3; auto v1 = sver.at(0).toInt(&ok1); auto v2 = sver.at(1).toInt(&ok2); auto v3 = sver.at(2).toInt(&ok3); if (ok1 && ok2 && ok3) m_jp2_version = QT_VERSION_CHECK(v1, v2, v3); } } ~JP2HandlerPrivate() { if (m_jp2_image) { opj_image_destroy(m_jp2_image); m_jp2_image = nullptr; } if (m_jp2_stream) { opj_stream_destroy(m_jp2_stream); m_jp2_stream = nullptr; } if (m_jp2_codec) { opj_destroy_codec(m_jp2_codec); m_jp2_codec = nullptr; } } /*! * \brief detectDecoderFormat * \param device * \return The codec JP2 found. */ OPJ_CODEC_FORMAT detectDecoderFormat(QIODevice *device) const { auto ba = device->peek(32); if (ba.left(12) == QByteArray::fromHex("0000000c6a5020200d0a870a")) { // if (ba.mid(20, 4) == QByteArray::fromHex("6a707820")) // 'jpx ' // return OPJ_CODEC_JPX; // JPEG 2000 Part 2 (not supported -> try reading as JP2) return OPJ_CODEC_JP2; } if (ba.left(5) == QByteArray::fromHex("ff4fff5100")) { return OPJ_CODEC_J2K; } return OPJ_CODEC_UNKNOWN; } bool createStream(QIODevice *device, bool read) { if (device == nullptr) { return false; } if (m_jp2_stream == nullptr) { m_jp2_stream = opj_stream_default_create(read ? OPJ_TRUE : OPJ_FALSE); } if (m_jp2_stream == nullptr) { return false; } opj_stream_set_user_data(m_jp2_stream, device, nullptr); opj_stream_set_user_data_length(m_jp2_stream, read ? device->size() : 0); opj_stream_set_read_function(m_jp2_stream, jp2_read); opj_stream_set_write_function(m_jp2_stream, jp2_write); opj_stream_set_skip_function(m_jp2_stream, jp2_skip); opj_stream_set_seek_function(m_jp2_stream, jp2_seek); return true; } bool isImageValid(const opj_image_t *i) const { return i && i->comps && i->numcomps > 0; } void enableThreads(opj_codec_t *codec) const { if (!opj_has_thread_support()) { qInfo() << "OpenJPEG doesn't support multi-threading!"; } else if (!opj_codec_set_threads(codec, std::max(1, QThread::idealThreadCount() / 2))) { qWarning() << "Unable to enable multi-threading!"; } } bool createDecoder(QIODevice *device) { if (m_jp2_codec) { return true; } auto jp2Format = detectDecoderFormat(device); if (jp2Format == OPJ_CODEC_UNKNOWN) { return false; } m_jp2_codec = opj_create_decompress(jp2Format); if (m_jp2_codec == nullptr) { return false; } enableThreads(m_jp2_codec); #ifdef QT_DEBUG // opj_set_info_handler(m_jp2_codec, info_callback, nullptr); // opj_set_warning_handler(m_jp2_codec, warning_callback, nullptr); #endif opj_set_error_handler(m_jp2_codec, error_callback, nullptr); return true; } bool readHeader(QIODevice *device) { if (!createStream(device, true)) { return false; } if (m_jp2_image) { return true; } if (!createDecoder(device)) { return false; } opj_set_default_decoder_parameters(&m_dparameters); if (!opj_setup_decoder(m_jp2_codec, &m_dparameters)) { qCritical() << "Failed to setup JP2 decoder!"; return false; } if (!opj_read_header(m_jp2_stream, m_jp2_codec, &m_jp2_image)) { qCritical() << "Failed to read JP2 header!"; return false; } return isImageValid(m_jp2_image); } template bool jp2ToImage(QImage *img) const { Q_ASSERT(img->depth() == 8 * sizeof(T) || img->depth() == 32 * sizeof(T)); for (qint32 c = 0, cc = m_jp2_image->numcomps; c < cc; ++c) { auto cs = cc == 1 ? 1 : 4; auto &&jc = m_jp2_image->comps[c]; if (jc.data == nullptr) return false; if (qint32(jc.w) != img->width() || qint32(jc.h) != img->height()) return false; // discriminate between int and float (avoid complicating things by creating classes with template specializations) if (std::numeric_limits::is_integer) { auto divisor = 1; if (jc.prec > sizeof(T) * 8) { // convert to the wanted precision (e.g. 16-bit -> 8-bit: divisor = 65535 / 255 = 257) divisor = std::max(1, int(((1ll << jc.prec) - 1) / ((1ll << (sizeof(T) * 8)) - 1))); } for (qint32 y = 0, h = img->height(); y < h; ++y) { auto ptr = reinterpret_cast(img->scanLine(y)); for (qint32 x = 0, w = img->width(); x < w; ++x) { qint32 v = jc.data[y * w + x] / divisor; if (jc.sgnd) // never seen v -= std::numeric_limits::type>::min(); *(ptr + x * cs + c) = std::clamp(v, qint32(std::numeric_limits::lowest()), qint32(std::numeric_limits::max())); } } } else { // float for (qint32 y = 0, h = img->height(); y < h; ++y) { auto ptr = reinterpret_cast(img->scanLine(y)); for (qint32 x = 0, w = img->width(); x < w; ++x) { *(ptr + x * cs + c) = jc.data[y * w + x]; } } } } return true; } template void alphaFix(QImage *img) const { if (m_jp2_image->numcomps == 3) { Q_ASSERT(img->depth() == 32 * sizeof(T)); for (qint32 y = 0, h = img->height(); y < h; ++y) { auto ptr = reinterpret_cast(img->scanLine(y)); for (qint32 x = 0, w = img->width(); x < w; ++x) { *(ptr + x * 4 + 3) = std::numeric_limits::is_iec559 ? 1 : std::numeric_limits::max(); } } } } QImage readImage(QIODevice *device) { if (!readHeader(device)) { return {}; } auto img = imageAlloc(size(), format()); if (img.isNull()) { return {}; } if (!opj_decode(m_jp2_codec, m_jp2_stream, m_jp2_image)) { qCritical() << "Failed to decoding JP2 image!"; return {}; } auto f = img.format(); if (f == QImage::Format_RGBA32FPx4 || f == QImage::Format_RGBX32FPx4) { if (!jp2ToImage(&img)) return {}; alphaFix(&img); } else if (f == QImage::Format_RGBA64 || f == QImage::Format_RGBX64 || f == QImage::Format_Grayscale16) { if (!jp2ToImage(&img)) return {}; alphaFix(&img); } else { if (!jp2ToImage(&img)) return {}; alphaFix(&img); } img.setColorSpace(colorSpace()); return img; } bool checkSizeLimits(qint32 width, qint32 height, qint32 nchannels) const { if (width > JP2_MAX_IMAGE_WIDTH || height > JP2_MAX_IMAGE_HEIGHT || width < 1 || height < 1) { qCritical() << "Maximum image size is limited to" << JP2_MAX_IMAGE_WIDTH << "x" << JP2_MAX_IMAGE_HEIGHT << "pixels"; return false; } if (qint64(width) * qint64(height) > JP2_MAX_IMAGE_PIXELS) { qCritical() << "Maximum image size is limited to" << JP2_MAX_IMAGE_PIXELS << "pixels"; return false; } // OpenJPEG uses a shadow copy @32-bit/channel so we need to do a check auto maxBytes = qint64(QImageReader::allocationLimit()) * 1024 * 1024; auto neededBytes = qint64(width) * height * nchannels * 4; if (maxBytes > 0 && neededBytes > maxBytes) { qCritical() << "Allocation limit set to" << (maxBytes / 1024 / 1024) << "MiB but" << (neededBytes / 1024 / 1024) << "MiB are needed!"; return false; } return true; } bool checkSizeLimits(const QSize &size, qint32 nchannels) const { return checkSizeLimits(size.width(), size.height(), nchannels); } QSize size() const { QSize sz; if (isImageValid(m_jp2_image)) { auto &&c0 = m_jp2_image->comps[0]; auto tmp = QSize(c0.w, c0.h); if (checkSizeLimits(tmp, m_jp2_image->numcomps)) sz = tmp; } return sz; } QImage::Format format() const { auto fmt = QImage::Format_Invalid; if (isImageValid(m_jp2_image)) { auto &&c0 = m_jp2_image->comps[0]; auto prec = c0.prec; for (quint32 c = 1; c < m_jp2_image->numcomps; ++c) { auto &&cc = m_jp2_image->comps[c]; if (cc.prec != prec) prec = 0; } auto jp2cs = m_jp2_image->color_space; #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) if (jp2cs == OPJ_CLRSPC_UNKNOWN || jp2cs == OPJ_CLRSPC_UNSPECIFIED) { auto cs = colorSpace(); if (cs.colorModel() == QColorSpace::ColorModel::Cmyk) jp2cs = OPJ_CLRSPC_CMYK; else if (cs.colorModel() == QColorSpace::ColorModel::Rgb) jp2cs = OPJ_CLRSPC_SRGB; else if (cs.colorModel() == QColorSpace::ColorModel::Gray) jp2cs = OPJ_CLRSPC_GRAY; } #endif if (jp2cs == OPJ_CLRSPC_UNKNOWN || jp2cs == OPJ_CLRSPC_UNSPECIFIED) { if (m_jp2_image->numcomps == 1) jp2cs = OPJ_CLRSPC_GRAY; else jp2cs = OPJ_CLRSPC_SRGB; } // *** IMPORTANT: To keep the code simple, the returned formats must have 1 or 4 channels (8/16/32-bits) if (jp2cs == OPJ_CLRSPC_SRGB) { if (m_jp2_image->numcomps == 3 || m_jp2_image->numcomps == 4) { auto hasAlpha = m_jp2_image->numcomps == 4; if (prec == 8) fmt = hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGBX8888; else if (prec == 16) fmt = hasAlpha ? QImage::Format_RGBA64 : QImage::Format_RGBX64; #ifdef JP2_ENABLE_HDR else if (prec == 32) // not sure about this fmt = hasAlpha ? QImage::Format_RGBA32FPx4 : QImage::Format_RGBX32FPx4; #endif } } else if (jp2cs == OPJ_CLRSPC_GRAY) { if (m_jp2_image->numcomps == 1) { if (prec == 8) fmt = QImage::Format_Grayscale8; else if (prec == 16) fmt = QImage::Format_Grayscale16; } } else if (jp2cs == OPJ_CLRSPC_CMYK) { if (m_jp2_image->numcomps == 4) { #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) if (prec == 8 || prec == 16) fmt = QImage::Format_CMYK8888; #endif } } } return fmt; } QColorSpace colorSpace() const { QColorSpace cs; if (m_jp2_image) { if (m_jp2_image->icc_profile_buf && m_jp2_image->icc_profile_len > 0) { cs = QColorSpace::fromIccProfile(QByteArray((char *)m_jp2_image->icc_profile_buf, m_jp2_image->icc_profile_len)); } if (!cs.isValid()) { if (m_jp2_image->color_space == OPJ_CLRSPC_SRGB) cs = QColorSpace(QColorSpace::SRgb); } } return cs; } /*! * \brief isSupported * \return True if the current JP2 image i ssupported by the plugin. Otherwise false. */ bool isSupported() const { return format() != QImage::Format_Invalid; } QByteArray subType() const { return m_subtype; } void setSubType(const QByteArray &type) { m_subtype = type; } qint32 quality() const { return m_quality; } void setQuality(qint32 quality) { m_quality = std::clamp(quality, -1, 100); } /*! * \brief encoderFormat * \return The encoder format set by subType. */ OPJ_CODEC_FORMAT encoderFormat() const { return subType() == J2K_SUBTYPE ? OPJ_CODEC_J2K : OPJ_CODEC_JP2; } /*! * \brief opjVersion * \return The runtime library version. */ qint32 opjVersion() const { return m_jp2_version; } bool imageToJp2(const QImage &image) { auto ncomp = image.hasAlphaChannel() ? 4 : 3; auto prec = 8; auto convFormat = image.format(); auto isFloat = false; auto cs = OPJ_CLRSPC_SRGB; if (opjVersion() >= QT_VERSION_CHECK(2, 5, 4)) { auto ics = image.colorSpace(); if (!(ics.isValid() && ics.primaries() == QColorSpace::Primaries::SRgb && ics.transferFunction() == QColorSpace::TransferFunction::SRgb)) { cs = OPJ_CLRSPC_UNKNOWN; } } switch (image.format()) { case QImage::Format_Mono: case QImage::Format_MonoLSB: case QImage::Format_Alpha8: case QImage::Format_Grayscale8: ncomp = 1; cs = OPJ_CLRSPC_GRAY; convFormat = QImage::Format_Grayscale8; break; case QImage::Format_Indexed8: if (image.isGrayscale()) { ncomp = 1; cs = OPJ_CLRSPC_GRAY; convFormat = QImage::Format_Grayscale8; } else { ncomp = 4; cs = OPJ_CLRSPC_SRGB; convFormat = QImage::Format_RGBA8888; } break; case QImage::Format_Grayscale16: ncomp = 1; prec = 16; cs = OPJ_CLRSPC_GRAY; convFormat = QImage::Format_Grayscale16; break; case QImage::Format_RGBX16FPx4: case QImage::Format_RGBX32FPx4: isFloat = true; #ifdef JP2_ENABLE_HDR prec = 32; convFormat = QImage::Format_RGBX32FPx4; cs = OPJ_CLRSPC_UNSPECIFIED; break; #else Q_FALLTHROUGH(); #endif case QImage::Format_RGBX64: case QImage::Format_RGB30: case QImage::Format_BGR30: prec = 16; convFormat = QImage::Format_RGBX64; break; case QImage::Format_RGBA16FPx4: case QImage::Format_RGBA16FPx4_Premultiplied: case QImage::Format_RGBA32FPx4: case QImage::Format_RGBA32FPx4_Premultiplied: isFloat = true; #ifdef JP2_ENABLE_HDR prec = 32; convFormat = QImage::Format_RGBA32FPx4; cs = OPJ_CLRSPC_UNSPECIFIED; break; #else Q_FALLTHROUGH(); #endif case QImage::Format_RGBA64: case QImage::Format_RGBA64_Premultiplied: case QImage::Format_A2RGB30_Premultiplied: case QImage::Format_A2BGR30_Premultiplied: prec = 16; convFormat = QImage::Format_RGBA64; break; #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) case QImage::Format_CMYK8888: // requires OpenJPEG 2.5.3+ if (opjVersion() >= QT_VERSION_CHECK(2, 5, 3)) { ncomp = 4; cs = OPJ_CLRSPC_CMYK; break; } else { Q_FALLTHROUGH(); } #endif default: if (image.depth() > 32) { qWarning() << "The image is saved losing precision!"; } convFormat = ncomp == 4 ? QImage::Format_RGBA8888 : QImage::Format_RGBX8888; break; } if (!checkSizeLimits(image.size(), ncomp)) { return false; } opj_set_default_encoder_parameters(&m_cparameters); m_cparameters.cod_format = encoderFormat(); m_cparameters.tile_size_on = 1; m_cparameters.cp_tdx = 1024; m_cparameters.cp_tdy = 1024; if (m_quality > -1 && m_quality < 100) { m_cparameters.irreversible = 1; m_cparameters.tcp_numlayers = 1; m_cparameters.cp_disto_alloc = 1; m_cparameters.tcp_rates[0] = 100.0 - (m_quality < 10 ? m_quality : 10 + (std::log10(m_quality) - 1) * 90); } std::unique_ptr cmptparm(new opj_image_cmptparm_t[ncomp]); for (int i = 0; i < ncomp; ++i) { auto &&p = cmptparm.get() + i; memset(p, 0, sizeof(opj_image_cmptparm_t)); p->dx = m_cparameters.subsampling_dx; p->dy = m_cparameters.subsampling_dy; p->w = image.width(); p->h = image.height(); p->x0 = 0; p->y0 = 0; p->prec = prec; p->sgnd = 0; } m_jp2_image = opj_image_create(ncomp, cmptparm.get(), cs); if (m_jp2_image == nullptr) { return false; } if (int(m_jp2_image->numcomps) != ncomp) { return false; // paranoia } m_jp2_image->x1 = image.width(); m_jp2_image->y1 = image.height(); ScanLineConverter scl(convFormat); if (prec < 32 && isFloat) { scl.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgbLinear)); } if (cs == OPJ_CLRSPC_SRGB) { scl.setTargetColorSpace(QColorSpace(QColorSpace::SRgb)); } else { scl.setTargetColorSpace(image.colorSpace()); } for (qint32 c = 0; c < ncomp; ++c) { auto &&comp = m_jp2_image->comps[c]; auto mul = ncomp == 1 ? 1 : 4; for (qint32 y = 0, h = image.height(); y < h; ++y) { if (prec == 8) { auto ptr = reinterpret_cast(scl.convertedScanLine(image, y)); for (qint32 x = 0, w = image.width(); x < w; ++x) comp.data[y * w + x] = ptr[x * mul + c]; } else if (prec == 16) { auto ptr = reinterpret_cast(scl.convertedScanLine(image, y)); for (qint32 x = 0, w = image.width(); x < w; ++x) comp.data[y * w + x] = ptr[x * mul + c]; } else if (prec == 32) { auto ptr = reinterpret_cast(scl.convertedScanLine(image, y)); for (qint32 x = 0, w = image.width(); x < w; ++x) comp.data[y * w + x] = ptr[x * mul + c]; } } } if (opjVersion() >= QT_VERSION_CHECK(2, 5, 4)) { auto colorSpace = scl.targetColorSpace().iccProfile(); if (!colorSpace.isEmpty()) { m_jp2_image->icc_profile_buf = reinterpret_cast(malloc(colorSpace.size())); if (m_jp2_image->icc_profile_buf) { memcpy(m_jp2_image->icc_profile_buf, colorSpace.data(), colorSpace.size()); m_jp2_image->icc_profile_len = colorSpace.size(); } } } return true; } bool writeImage(QIODevice *device, const QImage &image) { if (!imageToJp2(image)) { qCritical() << "Error while creating JP2 image!"; return false; } std::unique_ptr> codec(opj_create_compress(encoderFormat()), opj_destroy_codec); if (codec == nullptr) { qCritical() << "Error while creating encoder!"; return false; } enableThreads(codec.get()); #ifdef QT_DEBUG // opj_set_info_handler(m_jp2_codec, info_callback, nullptr); // opj_set_warning_handler(m_jp2_codec, warning_callback, nullptr); #endif opj_set_error_handler(m_jp2_codec, error_callback, nullptr); if (!opj_setup_encoder(codec.get(), &m_cparameters, m_jp2_image)) { return false; } if (!createStream(device, false)) { return false; } if (!opj_start_compress(codec.get(), m_jp2_image, m_jp2_stream)) { return false; } if (!opj_encode(codec.get(), m_jp2_stream)) { return false; } if (!opj_end_compress(codec.get(), m_jp2_stream)) { return false; } return true; } private: // common opj_stream_t *m_jp2_stream; opj_image_t *m_jp2_image; qint32 m_jp2_version; // read opj_codec_t *m_jp2_codec; opj_dparameters_t m_dparameters; // write opj_cparameters_t m_cparameters; qint32 m_quality; QByteArray m_subtype; }; JP2Handler::JP2Handler() : QImageIOHandler() , d(new JP2HandlerPrivate) { } bool JP2Handler::canRead() const { if (canRead(device())) { setFormat("jp2"); return true; } return false; } bool JP2Handler::canRead(QIODevice *device) { if (!device) { qWarning("JP2Handler::canRead() called with no device"); return false; } if (device->isSequential()) { return false; } JP2HandlerPrivate handler; return handler.detectDecoderFormat(device) != OPJ_CODEC_UNKNOWN; } bool JP2Handler::read(QImage *image) { auto dev = device(); if (dev == nullptr) { return false; } auto img = d->readImage(dev); if (img.isNull()) { return false; } *image = img; return true; } bool JP2Handler::write(const QImage &image) { if (image.isNull()) { return false; } auto dev = device(); if (dev == nullptr) { return false; } return d->writeImage(dev, image); } bool JP2Handler::supportsOption(ImageOption option) const { if (option == QImageIOHandler::Size) { return true; } if (option == QImageIOHandler::ImageFormat) { return true; } if (option == QImageIOHandler::SubType) { return true; } if (option == QImageIOHandler::SupportedSubTypes) { return true; } if (option == QImageIOHandler::Quality) { return true; } return false; } void JP2Handler::setOption(ImageOption option, const QVariant &value) { if (option == QImageIOHandler::SubType) { auto st = value.toByteArray(); if (this->option(QImageIOHandler::SupportedSubTypes).toList().contains(st)) d->setSubType(st); } if (option == QImageIOHandler::Quality) { auto ok = false; auto q = value.toInt(&ok); if (ok) { d->setQuality(q); } } } QVariant JP2Handler::option(ImageOption option) const { QVariant v; if (option == QImageIOHandler::Size) { if (d->readHeader(device())) { v = d->size(); } } if (option == QImageIOHandler::ImageFormat) { if (d->readHeader(device())) { v = d->format(); } } if (option == QImageIOHandler::SubType) { v = d->subType(); } if (option == QImageIOHandler::SupportedSubTypes) { v = QVariant::fromValue(QList() << JP2_SUBTYPE << J2K_SUBTYPE); } if (option == QImageIOHandler::Quality) { v = d->quality(); } return v; } QImageIOPlugin::Capabilities JP2Plugin::capabilities(QIODevice *device, const QByteArray &format) const { if (format == "jp2" || format == "j2k") { return Capabilities(CanRead | CanWrite); } // NOTE: JPF is the default extension of Photoshop for JP2 files. if (format == "jpf") { return Capabilities(CanRead); } if (!format.isEmpty()) { return {}; } if (!device->isOpen()) { return {}; } Capabilities cap; if (device->isReadable() && JP2Handler::canRead(device)) { cap |= CanRead; } if (device->isWritable()) { cap |= CanWrite; } return cap; } QImageIOHandler *JP2Plugin::create(QIODevice *device, const QByteArray &format) const { QImageIOHandler *handler = new JP2Handler; handler->setDevice(device); handler->setFormat(format); return handler; } #include "moc_jp2_p.cpp"