2022-10-12 00:42:06 +03:00
|
|
|
/* This file is part of the KDE libraries
|
|
|
|
Copyright (C) 2022 Ivailo Monev <xakepa10@gmail.com>
|
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
|
|
modify it under the terms of the GNU Library General Public
|
|
|
|
License version 2, as published by the Free Software Foundation.
|
|
|
|
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
Library General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
|
|
along with this library; see the file COPYING.LIB. If not, write to
|
|
|
|
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
|
|
Boston, MA 02110-1301, USA.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "jp2.h"
|
|
|
|
|
|
|
|
#include <QImage>
|
|
|
|
#include <qplatformdefs.h>
|
|
|
|
#include <kdebug.h>
|
|
|
|
|
|
|
|
#include <openjpeg.h>
|
|
|
|
|
|
|
|
static const char* const s_jp2pluginformat = "jp2";
|
|
|
|
static const OPJ_SIZE_T s_ojbuffersize = QT_BUFFSIZE;
|
|
|
|
|
|
|
|
static const ushort s_peekbuffsize = 32;
|
|
|
|
// for reference:
|
|
|
|
// https://en.wikipedia.org/wiki/List_of_file_signatures
|
2022-10-13 03:58:53 +03:00
|
|
|
// https://www.rfc-editor.org/rfc/rfc3745
|
|
|
|
// openjpeg/tests/j2k_random_tile_access.c
|
|
|
|
static const uchar s_jp2rfc3745header[] = { 0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a };
|
|
|
|
static const uchar s_jp2header[] = { 0x0d, 0x0a, 0x87, 0x0a };
|
2022-10-12 00:42:06 +03:00
|
|
|
static const uchar s_j2kheader[] = { 0xff, 0x4f, 0xff, 0x51 };
|
|
|
|
|
|
|
|
static const struct HeadersTblData {
|
|
|
|
const uchar *header;
|
|
|
|
const int headersize;
|
|
|
|
const OPJ_CODEC_FORMAT ojcodec;
|
|
|
|
} HeadersTbl[] = {
|
2022-10-13 03:58:53 +03:00
|
|
|
{ s_jp2rfc3745header, 12, OPJ_CODEC_JP2 },
|
|
|
|
{ s_jp2header, 4, OPJ_CODEC_JP2 },
|
2022-10-12 00:42:06 +03:00
|
|
|
{ s_j2kheader, 4, OPJ_CODEC_J2K }
|
|
|
|
};
|
|
|
|
static const qint16 HeadersTblSize = sizeof(HeadersTbl) / sizeof(HeadersTblData);
|
|
|
|
|
2022-10-12 23:25:00 +03:00
|
|
|
static OPJ_CODEC_FORMAT guessOJCodec(const QByteArray &data)
|
2022-10-12 00:42:06 +03:00
|
|
|
{
|
|
|
|
for (int i = 0; i < HeadersTblSize; i++) {
|
2022-10-12 23:25:00 +03:00
|
|
|
if (data.size() >= HeadersTbl[i].headersize &&
|
|
|
|
::memcmp(data.constData(), HeadersTbl[i].header, HeadersTbl[i].headersize) == 0) {
|
2022-10-12 00:42:06 +03:00
|
|
|
kDebug() << "Codec detected" << HeadersTbl[i].ojcodec;
|
|
|
|
return HeadersTbl[i].ojcodec;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return OPJ_CODEC_UNKNOWN;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void oj_info_callback(const char *msg, void *data)
|
|
|
|
{
|
|
|
|
Q_UNUSED(data);
|
|
|
|
kDebug() << msg;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void oj_warning_callback(const char *msg, void *data)
|
|
|
|
{
|
|
|
|
Q_UNUSED(data);
|
|
|
|
kWarning() << msg;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void oj_error_callback(const char *msg, void *data)
|
|
|
|
{
|
|
|
|
Q_UNUSED(data);
|
|
|
|
kError() << msg;
|
|
|
|
}
|
|
|
|
|
|
|
|
static OPJ_SIZE_T oj_read_callback(void *buffer, OPJ_SIZE_T size, void *data)
|
|
|
|
{
|
2022-10-13 04:09:10 +03:00
|
|
|
QIODevice* device = static_cast<QIODevice*>(data);
|
2022-10-12 00:42:06 +03:00
|
|
|
char* ojbuffer = static_cast<char*>(buffer);
|
2022-10-13 04:09:10 +03:00
|
|
|
const qint64 result = device->read(ojbuffer, size);
|
2022-10-12 00:42:06 +03:00
|
|
|
if (result == 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static OPJ_BOOL oj_seek_callback(OPJ_OFF_T size, void *data)
|
|
|
|
{
|
2022-10-13 04:09:10 +03:00
|
|
|
QIODevice* device = static_cast<QIODevice*>(data);
|
|
|
|
return device->seek(size);
|
2022-10-12 00:42:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
static OPJ_OFF_T oj_skip_callback(OPJ_OFF_T size, void *data)
|
|
|
|
{
|
2022-10-26 09:21:03 +03:00
|
|
|
QIODevice* device = static_cast<QIODevice*>(data);
|
|
|
|
device->seek(device->pos() + size);
|
|
|
|
return device->pos();
|
2022-10-12 00:42:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
JP2Handler::JP2Handler()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
bool JP2Handler::canRead() const
|
|
|
|
{
|
|
|
|
if (JP2Handler::canRead(device())) {
|
|
|
|
setFormat(s_jp2pluginformat);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool JP2Handler::read(QImage *image)
|
|
|
|
{
|
2022-10-13 03:55:40 +03:00
|
|
|
opj_stream_t *ojstream = opj_stream_create(s_ojbuffersize, OPJ_TRUE);
|
|
|
|
if (Q_UNLIKELY(!ojstream)) {
|
|
|
|
kWarning() << "Could not create stream";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-13 04:09:10 +03:00
|
|
|
opj_stream_set_user_data(ojstream, device(), NULL);
|
|
|
|
opj_stream_set_user_data_length(ojstream, device()->size());
|
|
|
|
opj_stream_set_read_function(ojstream, oj_read_callback);
|
|
|
|
opj_stream_set_seek_function(ojstream, oj_seek_callback);
|
|
|
|
opj_stream_set_skip_function(ojstream, oj_skip_callback);
|
|
|
|
|
|
|
|
opj_codec_t* ojcodec = opj_create_decompress(guessOJCodec(device()->peek(s_peekbuffsize)));
|
2022-10-13 03:14:57 +03:00
|
|
|
if (Q_UNLIKELY(!ojcodec)) {
|
2022-10-12 00:42:06 +03:00
|
|
|
kWarning() << "Could not create codec";
|
2022-10-13 03:55:40 +03:00
|
|
|
opj_stream_destroy(ojstream);
|
2022-10-12 00:42:06 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
opj_set_info_handler(ojcodec, oj_info_callback, NULL);
|
|
|
|
opj_set_warning_handler(ojcodec, oj_warning_callback, NULL);
|
|
|
|
opj_set_error_handler(ojcodec, oj_error_callback, NULL);
|
|
|
|
|
|
|
|
opj_dparameters_t ojparameters;
|
|
|
|
opj_set_default_decoder_parameters(&ojparameters);
|
|
|
|
ojparameters.m_verbose = true;
|
2022-10-13 03:55:40 +03:00
|
|
|
if (Q_UNLIKELY(opj_setup_decoder(ojcodec, &ojparameters) == OPJ_FALSE)) {
|
2022-10-13 07:36:30 +03:00
|
|
|
kWarning() << "Could not setup decoder";
|
2022-10-12 00:42:06 +03:00
|
|
|
opj_destroy_codec(ojcodec);
|
2022-10-13 03:55:40 +03:00
|
|
|
opj_stream_destroy(ojstream);
|
2022-10-12 00:42:06 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
opj_image_t* ojimage = NULL;
|
2022-10-13 03:14:57 +03:00
|
|
|
if (Q_UNLIKELY(opj_read_header(ojstream, ojcodec, &ojimage) == OPJ_FALSE)) {
|
2022-10-12 00:42:06 +03:00
|
|
|
kWarning() << "Could not read header";
|
|
|
|
opj_destroy_codec(ojcodec);
|
|
|
|
opj_stream_destroy(ojstream);
|
|
|
|
opj_image_destroy(ojimage);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-13 03:14:57 +03:00
|
|
|
if (Q_UNLIKELY(opj_decode(ojcodec, ojstream, ojimage) == OPJ_FALSE)) {
|
2022-10-12 00:42:06 +03:00
|
|
|
kWarning() << "Could not decode stream";
|
2022-10-13 03:55:40 +03:00
|
|
|
opj_destroy_codec(ojcodec);
|
|
|
|
opj_stream_destroy(ojstream);
|
|
|
|
opj_image_destroy(ojimage);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Q_UNLIKELY(opj_end_decompress(ojcodec, ojstream) == OPJ_FALSE)) {
|
2022-10-13 07:36:30 +03:00
|
|
|
kWarning() << "Could not end decompression";
|
2022-10-13 03:55:40 +03:00
|
|
|
opj_destroy_codec(ojcodec);
|
|
|
|
opj_stream_destroy(ojstream);
|
|
|
|
opj_image_destroy(ojimage);
|
|
|
|
return false;
|
2022-10-12 00:42:06 +03:00
|
|
|
}
|
|
|
|
|
2022-11-27 18:16:30 +02:00
|
|
|
switch (ojimage->color_space) {
|
|
|
|
case OPJ_CLRSPC_UNKNOWN:
|
|
|
|
case OPJ_CLRSPC_UNSPECIFIED: {
|
2022-11-29 02:46:38 +02:00
|
|
|
// NOTE: there are images with unspecified or unknown color space that are sRGB but
|
2022-11-27 18:16:30 +02:00
|
|
|
// in case it is not sRGB and it looks weird when it is loaded tell the person who
|
|
|
|
// created the image to fix it somehow
|
|
|
|
kDebug() << "Unspecified or unknown color space";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OPJ_CLRSPC_SRGB:
|
|
|
|
case OPJ_CLRSPC_GRAY: {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
kWarning() << "Unsupported color space" << ojimage->color_space;
|
|
|
|
opj_destroy_codec(ojcodec);
|
|
|
|
opj_stream_destroy(ojstream);
|
|
|
|
opj_image_destroy(ojimage);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-27 18:23:35 +02:00
|
|
|
*image = QImage(ojimage->comps->w, ojimage->comps->h, QImage::Format_ARGB32);
|
|
|
|
if (Q_UNLIKELY(image->isNull())) {
|
|
|
|
kWarning() << "Could not create image";
|
|
|
|
opj_destroy_codec(ojcodec);
|
|
|
|
opj_stream_destroy(ojstream);
|
|
|
|
opj_image_destroy(ojimage);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-12 00:42:06 +03:00
|
|
|
switch (ojimage->numcomps) {
|
|
|
|
case 4: {
|
|
|
|
QRgb* imagebits = reinterpret_cast<QRgb*>(image->bits());
|
|
|
|
const uint bitscount = (ojimage->comps->h * ojimage->comps->w * ojimage->numcomps);
|
2022-10-12 04:58:55 +03:00
|
|
|
OPJ_INT32* ojr = ojimage->comps[0].data;
|
|
|
|
OPJ_INT32* ojg = ojimage->comps[1].data;
|
|
|
|
OPJ_INT32* ojb = ojimage->comps[2].data;
|
|
|
|
OPJ_INT32* oja = ojimage->comps[3].data;
|
2022-10-12 00:42:06 +03:00
|
|
|
for (uint i = 0 ; i < bitscount; i += ojimage->numcomps) {
|
2022-10-12 04:58:55 +03:00
|
|
|
*imagebits = qRgba(*ojr, *ojg, *ojb, *oja);
|
|
|
|
ojr++;
|
|
|
|
ojg++;
|
|
|
|
ojb++;
|
|
|
|
oja++;
|
2022-10-12 00:42:06 +03:00
|
|
|
imagebits++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 3: {
|
|
|
|
QRgb* imagebits = reinterpret_cast<QRgb*>(image->bits());
|
|
|
|
const uint bitscount = (ojimage->comps->h * ojimage->comps->w * ojimage->numcomps);
|
2022-10-12 04:58:55 +03:00
|
|
|
OPJ_INT32* ojr = ojimage->comps[0].data;
|
|
|
|
OPJ_INT32* ojg = ojimage->comps[1].data;
|
|
|
|
OPJ_INT32* ojb = ojimage->comps[2].data;
|
2022-10-12 00:42:06 +03:00
|
|
|
for (uint i = 0 ; i < bitscount; i += ojimage->numcomps) {
|
2022-10-12 04:58:55 +03:00
|
|
|
*imagebits = qRgba(*ojr, *ojg, *ojb, 0xff);
|
|
|
|
ojr++;
|
|
|
|
ojg++;
|
|
|
|
ojb++;
|
|
|
|
imagebits++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// gray with alpha
|
|
|
|
case 2: {
|
|
|
|
QRgb* imagebits = reinterpret_cast<QRgb*>(image->bits());
|
|
|
|
const uint bitscount = (ojimage->comps->h * ojimage->comps->w * ojimage->numcomps);
|
|
|
|
OPJ_INT32* ojg = ojimage->comps[0].data;
|
|
|
|
OPJ_INT32* oja = ojimage->comps[1].data;
|
|
|
|
for (uint i = 0 ; i < bitscount; i += ojimage->numcomps) {
|
|
|
|
*imagebits = qRgba(*ojg, *ojg, *ojg, *oja);
|
|
|
|
ojg++;
|
|
|
|
oja++;
|
|
|
|
imagebits++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// gray only
|
|
|
|
case 1: {
|
|
|
|
QRgb* imagebits = reinterpret_cast<QRgb*>(image->bits());
|
|
|
|
const uint bitscount = (ojimage->comps->h * ojimage->comps->w * ojimage->numcomps);
|
|
|
|
OPJ_INT32* ojg = ojimage->comps[0].data;
|
|
|
|
for (uint i = 0 ; i < bitscount; i += ojimage->numcomps) {
|
|
|
|
*imagebits = qRgba(*ojg, *ojg, *ojg, 0xff);
|
|
|
|
ojg++;
|
2022-10-12 00:42:06 +03:00
|
|
|
imagebits++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
2022-10-12 04:58:55 +03:00
|
|
|
kWarning() << "Invalid color component count" << ojimage->numcomps;
|
2022-10-12 00:42:06 +03:00
|
|
|
*image = QImage();
|
2022-10-30 01:34:49 +03:00
|
|
|
opj_destroy_codec(ojcodec);
|
|
|
|
opj_stream_destroy(ojstream);
|
|
|
|
opj_image_destroy(ojimage);
|
|
|
|
return false;
|
2022-10-12 00:42:06 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
opj_destroy_codec(ojcodec);
|
|
|
|
opj_stream_destroy(ojstream);
|
|
|
|
opj_image_destroy(ojimage);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool JP2Handler::write(const QImage &image)
|
|
|
|
{
|
|
|
|
// this plugin is a read-only kind of plugin
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray JP2Handler::name() const
|
|
|
|
{
|
|
|
|
return s_jp2pluginformat;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool JP2Handler::canRead(QIODevice *device)
|
|
|
|
{
|
|
|
|
if (Q_UNLIKELY(!device)) {
|
|
|
|
kWarning() << "Called with no device";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QByteArray data = device->peek(s_peekbuffsize);
|
|
|
|
|
|
|
|
if (Q_UNLIKELY(data.isEmpty())) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-12 23:25:00 +03:00
|
|
|
return (guessOJCodec(data) != OPJ_CODEC_UNKNOWN);
|
2022-10-12 00:42:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
QList<QByteArray> JP2Plugin::mimeTypes() const
|
|
|
|
{
|
|
|
|
static const QList<QByteArray> list = QList<QByteArray>()
|
2022-10-13 07:38:38 +03:00
|
|
|
<< "image/jp2"
|
2022-10-26 07:57:42 +03:00
|
|
|
<< "image/x-jp2-codestream"
|
2022-10-26 19:06:46 +03:00
|
|
|
<< "image/jpx";
|
2022-10-12 00:42:06 +03:00
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
QImageIOPlugin::Capabilities JP2Plugin::capabilities(QIODevice *device, const QByteArray &format) const
|
|
|
|
{
|
2022-10-13 14:18:29 +03:00
|
|
|
if (format == s_jp2pluginformat) {
|
2022-10-12 00:42:06 +03:00
|
|
|
return QImageIOPlugin::Capabilities(QImageIOPlugin::CanRead);
|
2022-10-13 14:18:29 +03:00
|
|
|
}
|
|
|
|
if (!device || !device->isOpen()) {
|
2022-10-12 00:42:06 +03:00
|
|
|
return 0;
|
2022-10-13 14:18:29 +03:00
|
|
|
}
|
|
|
|
if (device->isReadable() && JP2Handler::canRead(device)) {
|
|
|
|
return QImageIOPlugin::Capabilities(QImageIOPlugin::CanRead);
|
|
|
|
}
|
|
|
|
return 0;
|
2022-10-12 00:42:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
QImageIOHandler *JP2Plugin::create(QIODevice *device, const QByteArray &format) const
|
|
|
|
{
|
|
|
|
QImageIOHandler *handler = new JP2Handler();
|
|
|
|
handler->setDevice(device);
|
|
|
|
handler->setFormat(format);
|
|
|
|
return handler;
|
|
|
|
}
|
|
|
|
|
2022-11-14 15:50:30 +02:00
|
|
|
Q_EXPORT_PLUGIN(JP2Plugin)
|