kimageformats/src/imageformats/jp2.cpp
2025-02-17 08:54:49 +01:00

897 lines
27 KiB
C++

/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "jp2_p.h"
#include "scanlineconverter_p.h"
#include "util_p.h"
#include <QColorSpace>
#include <QIODevice>
#include <QImage>
#include <QImageReader>
#include <QThread>
#include <openjpeg.h>
/* *** 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<qint32>::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<class T>
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<T>::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<T *>(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<typename std::make_signed<T>::type>::min();
*(ptr + x * cs + c) = std::clamp(v, qint32(std::numeric_limits<T>::lowest()), qint32(std::numeric_limits<T>::max()));
}
}
} else { // float
for (qint32 y = 0, h = img->height(); y < h; ++y) {
auto ptr = reinterpret_cast<T *>(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<class T>
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<T *>(img->scanLine(y));
for (qint32 x = 0, w = img->width(); x < w; ++x) {
*(ptr + x * 4 + 3) = std::numeric_limits<T>::is_iec559 ? 1 : std::numeric_limits<T>::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<quint32>(&img))
return {};
alphaFix<float>(&img);
} else if (f == QImage::Format_RGBA64 || f == QImage::Format_RGBX64 || f == QImage::Format_Grayscale16) {
if (!jp2ToImage<quint16>(&img))
return {};
alphaFix<quint16>(&img);
} else {
if (!jp2ToImage<quint8>(&img))
return {};
alphaFix<quint8>(&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<opj_image_cmptparm_t> 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<const quint8 *>(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<const quint16 *>(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<const quint32 *>(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<OPJ_BYTE *>(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<opj_codec_t, std::function<void(opj_codec_t *)>> 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<QByteArray>() << 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"