diff --git a/CMakeLists.txt b/CMakeLists.txt index f7f333a0..e6a60d0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,6 +193,13 @@ set_package_properties(LibJPEG PROPERTIES TYPE OPTIONAL ) +kde4_optional_find_package(OpenJPEG) +set_package_properties(OpenJPEG PROPERTIES + DESCRIPTION "Open-source JPEG 2000 codec written in C language" + URL "https://www.openjpeg.org/" + PURPOSE "Support for JPEG 2000 image format" + TYPE OPTIONAL +) # v143+ required for udev_monitor_filter_add_match_subsystem_devtype() kde4_optional_find_package(UDev 143) set_package_properties(UDev PROPERTIES diff --git a/cmake/modules/FindOpenJPEG.cmake b/cmake/modules/FindOpenJPEG.cmake new file mode 100644 index 00000000..eb450f3e --- /dev/null +++ b/cmake/modules/FindOpenJPEG.cmake @@ -0,0 +1,40 @@ +# Try to find OpenJPEG library, once done this will define: +# +# OPENJPEG_FOUND - system has OpenJPEG +# OPENJPEG_INCLUDE_DIR - the OpenJPEG include directory +# OPENJPEG_LIBRARIES - the libraries needed to use OpenJPEG +# OPENJPEG_DEFINITIONS - compiler switches required for using OpenJPEG +# +# Copyright (c) 2022 Ivailo Monev +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(PC_OPENJPEG QUIET libopenjp2) + +set(OPENJPEG_INCLUDE_DIR ${PC_OPENJPEG_INCLUDE_DIRS}) +set(OPENJPEG_LIBRARIES ${PC_OPENJPEG_LIBRARIES}) +set(OPENJPEG_VERSION ${PC_OPENJPEG_VERSION}) +set(OPENJPEG_DEFINITIONS ${PC_OPENJPEG_CFLAGS_OTHER}) + +if(NOT OPENJPEG_INCLUDE_DIR OR NOT OPENJPEG_LIBRARIES) + find_path(OPENJPEG_INCLUDE_DIR + NAMES openjpeg.h + PATH_SUFFIXES openjpeg + HINTS $ENV{OPENJPEGDIR}/include + ) + + find_library(OPENJPEG_LIBRARIES + NAMES openjp2 + HINTS $ENV{OPENJPEGDIR}/lib + ) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpenJPEG + VERSION_VAR OPENJPEG_VERSION + REQUIRED_VARS OPENJPEG_LIBRARIES OPENJPEG_INCLUDE_DIR +) + +mark_as_advanced(OPENJPEG_INCLUDE_DIR OPENJPEG_LIBRARIES) diff --git a/kdeui/util/kimageio.h b/kdeui/util/kimageio.h index 8b8211f6..08a4455c 100644 --- a/kdeui/util/kimageio.h +++ b/kdeui/util/kimageio.h @@ -27,11 +27,11 @@ * @li WEBP \ \ * @li RAW \ * @li JPEG \ + * @li JP2 \ * @li PGM \ * @li XBM \ * @li BMP \ * @li ICO \ - * @li JP2 \ * @li GIF \ */ namespace KImageIO diff --git a/kimgio/AUTHORS b/kimgio/AUTHORS index 84e95409..ded46515 100644 --- a/kimgio/AUTHORS +++ b/kimgio/AUTHORS @@ -1,3 +1,3 @@ Sirtaj Singh Kang -- kimgio and krl readers Antonio Larossa -- initial version of KRL reader -Ivailo Monev -- magick, raw and jpeg plugins +Ivailo Monev -- magick, raw, jpeg and jp2 plugins diff --git a/kimgio/CMakeLists.txt b/kimgio/CMakeLists.txt index 74cba83b..e0799981 100644 --- a/kimgio/CMakeLists.txt +++ b/kimgio/CMakeLists.txt @@ -83,3 +83,23 @@ if(LIBJPEG_FOUND) DESTINATION ${KDE4_PLUGIN_INSTALL_DIR}/plugins/imageformats ) endif(LIBJPEG_FOUND) + +################################## + +if(OPENJPEG_FOUND) + include_directories(${OPENJPEG_INCLUDE_DIR}) + kde4_add_plugin(kimg_jp2 jp2.cpp) + target_link_libraries(kimg_jp2 + ${KDE4_KDECORE_LIBS} + ${QT_QTGUI_LIBRARY} + ${OPENJPEG_LIBRARIES} + ) + set_target_properties(kimg_jp2 PROPERTIES + OUTPUT_NAME jp2 + ) + + install( + TARGETS kimg_jp2 + DESTINATION ${KDE4_PLUGIN_INSTALL_DIR}/plugins/imageformats + ) +endif(OPENJPEG_FOUND) diff --git a/kimgio/README b/kimgio/README index cd64d5e9..0e28ecfd 100644 --- a/kimgio/README +++ b/kimgio/README @@ -6,6 +6,7 @@ Current formats include: WEBP RAW JPEG +JP2 along with whatever ImageMagick supports but it is assumed that it supports reading atleast the following formats (even if they cannot be loaded because @@ -15,7 +16,6 @@ PGM XBM BMP ICO -JP2 GIF If you want to contribute plugin for image format and there is no solid diff --git a/kimgio/jp2.cpp b/kimgio/jp2.cpp new file mode 100644 index 00000000..3a3ea56d --- /dev/null +++ b/kimgio/jp2.cpp @@ -0,0 +1,283 @@ +/* This file is part of the KDE libraries + Copyright (C) 2022 Ivailo Monev + + 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 +#include +#include + +#include + +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 +static const uchar s_jp2header[] = { 0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a }; +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[] = { + { s_jp2header, 12, OPJ_CODEC_JP2 }, + { s_j2kheader, 4, OPJ_CODEC_J2K } +}; +static const qint16 HeadersTblSize = sizeof(HeadersTbl) / sizeof(HeadersTblData); + +static OPJ_CODEC_FORMAT guessOJCodec(const char* const data) +{ + for (int i = 0; i < HeadersTblSize; i++) { + if (qstrncmp(data, reinterpret_cast(HeadersTbl[i].header), HeadersTbl[i].headersize) == 0) { + 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) +{ + JP2Handler* jp2handler = static_cast(data); + char* ojbuffer = static_cast(buffer); + const qint64 result = jp2handler->device()->read(ojbuffer, size); + if (result == 0) { + return -1; + } + return result; +} + +static OPJ_BOOL oj_seek_callback(OPJ_OFF_T size, void *data) +{ + JP2Handler* jp2handler = static_cast(data); + return jp2handler->device()->seek(size); +} + +static OPJ_OFF_T oj_skip_callback(OPJ_OFF_T size, void *data) +{ + kWarning() << "Not implemented"; + Q_UNUSED(size); + Q_UNUSED(data); + return 0; +} + + +JP2Handler::JP2Handler() +{ +} + +JP2Handler::~JP2Handler() +{ +} + +bool JP2Handler::canRead() const +{ + if (JP2Handler::canRead(device())) { + setFormat(s_jp2pluginformat); + return true; + } + return false; +} + +bool JP2Handler::read(QImage *image) +{ + const qint64 devicepos = device()->pos(); + const QByteArray data = device()->readAll(); + device()->seek(devicepos); + + opj_codec_t* ojcodec = opj_create_decompress(guessOJCodec(data.constData())); + if (!ojcodec) { + kWarning() << "Could not create codec"; + 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; + + opj_setup_decoder(ojcodec, &ojparameters); + + opj_stream_t *ojstream = opj_stream_create(s_ojbuffersize, OPJ_TRUE); + if (!ojstream) { + kWarning() << "Could not create stream"; + opj_destroy_codec(ojcodec); + return false; + } + + opj_stream_set_user_data_length(ojstream, data.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_stream_set_user_data(ojstream, this, NULL); + + opj_image_t* ojimage = NULL; + if (opj_read_header(ojstream, ojcodec, &ojimage) == OPJ_FALSE) { + kWarning() << "Could not read header"; + opj_destroy_codec(ojcodec); + opj_stream_destroy(ojstream); + opj_image_destroy(ojimage); + return false; + } + + if (opj_decode(ojcodec, ojstream, ojimage) == OPJ_FALSE) { + kWarning() << "Could not decode stream"; + } + opj_end_decompress(ojcodec, ojstream); + + *image = QImage(ojimage->comps->w, ojimage->comps->h, QImage::Format_ARGB32); + if (image->isNull()) { + kWarning() << "Could not create image QImage"; + opj_destroy_codec(ojcodec); + opj_stream_destroy(ojstream); + opj_image_destroy(ojimage); + return false; + } + + switch (ojimage->numcomps) { + case 4: { + QRgb* imagebits = reinterpret_cast(image->bits()); + const uint bitscount = (ojimage->comps->h * ojimage->comps->w * ojimage->numcomps); + OPJ_INT32* r = ojimage->comps[0].data; + OPJ_INT32* g = ojimage->comps[1].data; + OPJ_INT32* b = ojimage->comps[2].data; + OPJ_INT32* a = ojimage->comps[3].data; + for (uint i = 0 ; i < bitscount; i += ojimage->numcomps) { + *imagebits = qRgba(*r, *g, *b, *a); + r++; + g++; + b++; + a++; + imagebits++; + } + break; + } + case 3: { + QRgb* imagebits = reinterpret_cast(image->bits()); + const uint bitscount = (ojimage->comps->h * ojimage->comps->w * ojimage->numcomps); + OPJ_INT32* r = ojimage->comps[0].data; + OPJ_INT32* g = ojimage->comps[1].data; + OPJ_INT32* b = ojimage->comps[2].data; + for (uint i = 0 ; i < bitscount; i += ojimage->numcomps) { + *imagebits = qRgba(*r, *g, *b, 0xff); + r++; + g++; + b++; + imagebits++; + } + break; + } + default: { + kWarning() << "Unsupported color component count" << ojimage->numcomps; + *image = QImage(); + break; + } + } + + 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; + } + + return (guessOJCodec(data.constData()) != OPJ_CODEC_UNKNOWN); +} + +QStringList JP2Plugin::keys() const +{ + return QStringList() << s_jp2pluginformat; +} + +QList JP2Plugin::mimeTypes() const +{ + static const QList list = QList() + << "image/jp2"; + return list; +} + +QImageIOPlugin::Capabilities JP2Plugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == s_jp2pluginformat) + return QImageIOPlugin::Capabilities(QImageIOPlugin::CanRead); + if (!device->isOpen()) + return 0; + + QImageIOPlugin::Capabilities cap; + if (device->isReadable() && JP2Handler::canRead(device)) + cap |= QImageIOPlugin::CanRead; + return cap; +} + +QImageIOHandler *JP2Plugin::create(QIODevice *device, const QByteArray &format) const +{ + QImageIOHandler *handler = new JP2Handler(); + handler->setDevice(device); + handler->setFormat(format); + return handler; +} + +Q_EXPORT_PLUGIN2(jp2, JP2Plugin) diff --git a/kimgio/jp2.h b/kimgio/jp2.h new file mode 100644 index 00000000..f07d7387 --- /dev/null +++ b/kimgio/jp2.h @@ -0,0 +1,50 @@ +/* This file is part of the KDE libraries + Copyright (C) 2022 Ivailo Monev + + 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. +*/ + +#ifndef KIMG_JP2_H +#define KIMG_JP2_H + +#include +#include + +class JP2Handler : public QImageIOHandler +{ +public: + JP2Handler(); + ~JP2Handler(); + + bool canRead() const final; + bool read(QImage *image) final; + bool write(const QImage &image) final; + + QByteArray name() const final; + + static bool canRead(QIODevice *device); +}; + +class JP2Plugin : public QImageIOPlugin +{ +public: + QStringList keys() const; + QList mimeTypes() const; + Capabilities capabilities(QIODevice *device, const QByteArray &format) const final; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const final; +}; + +#endif // KIMG_JP2_H +