diff --git a/CMakeLists.txt b/CMakeLists.txt index 64a67ce..6014cd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,13 @@ if(KIMAGEFORMATS_JXL) endif() add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images") +option(KIMAGEFORMATS_JP2 "Enable plugin for JPEG 2000 format" ON) +if(KIMAGEFORMATS_JP2) + find_package(OpenJPEG CONFIG) +endif() +add_feature_info(OpenJPEG OpenJPEG_FOUND "required for the QImage plugin for JPEG 2000 images") + + find_package(LibRaw 0.20.2) set_package_properties(LibRaw PROPERTIES TYPE OPTIONAL diff --git a/README.md b/README.md index 14c6354..6ffb7ef 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The following image formats have read and write support: - DirectDraw Surface (dds) - Encapsulated PostScript (eps) - High Efficiency Image File Format (heif) +- JPEG 2000 (jp2, j2k, jpf) - JPEG XL (jxl) - JPEG XR (jxr) - OpenEXR (exr) @@ -75,6 +76,12 @@ The DDS plugin is a fork from Qt 5.6 with bug fixes and improvements. The plugin was forked because Qt Project no longer supports its DDS plugin. +### The JP2 plugin + +The JP2 plugin is based on the popular and wide used OpenJPEG library. + +The Qt project has a no longer supported JPEG 2000 plugin based on Jasper. + ## License This framework is licensed under the @@ -134,6 +141,7 @@ plugin ('n/a' means no limit, i.e. the limit depends on the format encoding). - EPS: n/a - HDR: n/a (large image) - HEIF: n/a +- JP2: 300,000 x 300,000 pixels - JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels - JXR: n/a, in any case no larger than 4 GB - KRA: same size as Qt's PNG plugin @@ -178,8 +186,8 @@ been used or the maximum size of the image that can be saved has been limited. PSD plugin loads CMYK, Lab and Multichannel images and converts them to RGB without using the ICC profile. -JXR, PSD and SCT plugins natively support 4-channel CMYK images when compiled -with Qt 6.8+. +JP2, JXR, PSD and SCT plugins natively support 4-channel CMYK images when +compiled with Qt 6.8+. ### The DDS plugin @@ -218,6 +226,15 @@ create a temporary PDF file which is then converted to EPS. Therefore, if The following defines can be defined in cmake to modify the behavior of the plugin: - `HDR_HALF_QUALITY`: on read, a 16-bit float image is returned instead of a 32-bit float one. +### The JP2 plugin + +**This plugin can be disabled by setting `KIMAGEFORMATS_JP2` to `OFF` +in your cmake options.** + +JP2 plugin has the following limitations due to the lack of support by OpenJPEG: +- Metadata are not supported +- Image resolution is not supported + ### The JXL plugin **The current version of the plugin limits the image size to 256 megapixels diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 17fa98f..a432dfa 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -119,6 +119,18 @@ if (LibHeif_FOUND) endif() endif() +if (OpenJPEG_FOUND) + kimageformats_read_tests( + jp2 + ) + # CMYK writing is incorrect in versions before 2.5.3 + if ("${OPENJPEG_MAJOR_VERSION}.${OPENJPEG_MINOR_VERSION}.${OPENJPEG_BUILD_VERSION}" VERSION_GREATER_EQUAL "2.5.3") + kimageformats_write_tests( + jp2-nodatacheck-lossless + ) + endif() +endif() + if (LibJXL_FOUND AND LibJXLThreads_FOUND) kimageformats_read_tests( jxl diff --git a/autotests/read/jp2/testcard_cmyk.jp2 b/autotests/read/jp2/testcard_cmyk.jp2 new file mode 100644 index 0000000..3ca3be3 Binary files /dev/null and b/autotests/read/jp2/testcard_cmyk.jp2 differ diff --git a/autotests/read/jp2/testcard_cmyk.jp2.json b/autotests/read/jp2/testcard_cmyk.jp2.json new file mode 100644 index 0000000..01b07a9 --- /dev/null +++ b/autotests/read/jp2/testcard_cmyk.jp2.json @@ -0,0 +1,11 @@ +[ + { + "minQtVersion" : "6.8.0", + "fileName" : "testcard_cmyk.tif" + }, + { + "maxQtVersion" : "6.7.99", + "unsupportedFormat" : true, + "comment" : "Qt versions lower than 6.8 do not support CMYK format so this test should be skipped." + } +] diff --git a/autotests/read/jp2/testcard_cmyk.tif b/autotests/read/jp2/testcard_cmyk.tif new file mode 100644 index 0000000..1e6c025 Binary files /dev/null and b/autotests/read/jp2/testcard_cmyk.tif differ diff --git a/autotests/read/jp2/testcard_cmyk16.jp2 b/autotests/read/jp2/testcard_cmyk16.jp2 new file mode 100644 index 0000000..bb7d1ea Binary files /dev/null and b/autotests/read/jp2/testcard_cmyk16.jp2 differ diff --git a/autotests/read/jp2/testcard_cmyk16.jp2.json b/autotests/read/jp2/testcard_cmyk16.jp2.json new file mode 100644 index 0000000..667f1f9 --- /dev/null +++ b/autotests/read/jp2/testcard_cmyk16.jp2.json @@ -0,0 +1,11 @@ +[ + { + "minQtVersion" : "6.8.0", + "fileName" : "testcard_cmyk16.tif" + }, + { + "maxQtVersion" : "6.7.99", + "unsupportedFormat" : true, + "comment" : "Qt versions lower than 6.8 do not support CMYK format so this test should be skipped." + } +] diff --git a/autotests/read/jp2/testcard_cmyk16.tif b/autotests/read/jp2/testcard_cmyk16.tif new file mode 100644 index 0000000..994593b Binary files /dev/null and b/autotests/read/jp2/testcard_cmyk16.tif differ diff --git a/autotests/read/jp2/testcard_gray.jp2 b/autotests/read/jp2/testcard_gray.jp2 new file mode 100644 index 0000000..6ba9336 Binary files /dev/null and b/autotests/read/jp2/testcard_gray.jp2 differ diff --git a/autotests/read/jp2/testcard_gray.png b/autotests/read/jp2/testcard_gray.png new file mode 100644 index 0000000..e098630 Binary files /dev/null and b/autotests/read/jp2/testcard_gray.png differ diff --git a/autotests/read/jp2/testcard_gray16.jp2 b/autotests/read/jp2/testcard_gray16.jp2 new file mode 100644 index 0000000..b6aa4ec Binary files /dev/null and b/autotests/read/jp2/testcard_gray16.jp2 differ diff --git a/autotests/read/jp2/testcard_gray16.png b/autotests/read/jp2/testcard_gray16.png new file mode 100644 index 0000000..537c11b Binary files /dev/null and b/autotests/read/jp2/testcard_gray16.png differ diff --git a/autotests/read/jp2/testcard_rgb.jp2 b/autotests/read/jp2/testcard_rgb.jp2 new file mode 100644 index 0000000..934bb27 Binary files /dev/null and b/autotests/read/jp2/testcard_rgb.jp2 differ diff --git a/autotests/read/jp2/testcard_rgb.png b/autotests/read/jp2/testcard_rgb.png new file mode 100644 index 0000000..f44508c Binary files /dev/null and b/autotests/read/jp2/testcard_rgb.png differ diff --git a/autotests/read/jp2/testcard_rgb16.jp2 b/autotests/read/jp2/testcard_rgb16.jp2 new file mode 100644 index 0000000..5e241c1 Binary files /dev/null and b/autotests/read/jp2/testcard_rgb16.jp2 differ diff --git a/autotests/read/jp2/testcard_rgb16.png b/autotests/read/jp2/testcard_rgb16.png new file mode 100644 index 0000000..0f049ed Binary files /dev/null and b/autotests/read/jp2/testcard_rgb16.png differ diff --git a/autotests/read/jp2/testcard_rgba.j2k b/autotests/read/jp2/testcard_rgba.j2k new file mode 100644 index 0000000..ec4a948 Binary files /dev/null and b/autotests/read/jp2/testcard_rgba.j2k differ diff --git a/autotests/read/jp2/testcard_rgba.jp2 b/autotests/read/jp2/testcard_rgba.jp2 new file mode 100644 index 0000000..f0f7c3f Binary files /dev/null and b/autotests/read/jp2/testcard_rgba.jp2 differ diff --git a/autotests/read/jp2/testcard_rgba.png b/autotests/read/jp2/testcard_rgba.png new file mode 100644 index 0000000..2454247 Binary files /dev/null and b/autotests/read/jp2/testcard_rgba.png differ diff --git a/autotests/read/jp2/testcard_rgba16.jp2 b/autotests/read/jp2/testcard_rgba16.jp2 new file mode 100644 index 0000000..00f9e16 Binary files /dev/null and b/autotests/read/jp2/testcard_rgba16.jp2 differ diff --git a/autotests/read/jp2/testcard_rgba16.png b/autotests/read/jp2/testcard_rgba16.png new file mode 100644 index 0000000..928ca49 Binary files /dev/null and b/autotests/read/jp2/testcard_rgba16.png differ diff --git a/autotests/write/format/jp2/Format_A2BGR30_Premultiplied.jp2 b/autotests/write/format/jp2/Format_A2BGR30_Premultiplied.jp2 new file mode 100644 index 0000000..ead783c Binary files /dev/null and b/autotests/write/format/jp2/Format_A2BGR30_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_A2RGB30_Premultiplied.jp2 b/autotests/write/format/jp2/Format_A2RGB30_Premultiplied.jp2 new file mode 100644 index 0000000..ead783c Binary files /dev/null and b/autotests/write/format/jp2/Format_A2RGB30_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_ARGB32.jp2 b/autotests/write/format/jp2/Format_ARGB32.jp2 new file mode 100644 index 0000000..7ec4354 Binary files /dev/null and b/autotests/write/format/jp2/Format_ARGB32.jp2 differ diff --git a/autotests/write/format/jp2/Format_ARGB32_Premultiplied.jp2 b/autotests/write/format/jp2/Format_ARGB32_Premultiplied.jp2 new file mode 100644 index 0000000..a75f69b Binary files /dev/null and b/autotests/write/format/jp2/Format_ARGB32_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_ARGB4444_Premultiplied.jp2 b/autotests/write/format/jp2/Format_ARGB4444_Premultiplied.jp2 new file mode 100644 index 0000000..d51e4cb Binary files /dev/null and b/autotests/write/format/jp2/Format_ARGB4444_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_ARGB6666_Premultiplied.jp2 b/autotests/write/format/jp2/Format_ARGB6666_Premultiplied.jp2 new file mode 100644 index 0000000..5bcd672 Binary files /dev/null and b/autotests/write/format/jp2/Format_ARGB6666_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_ARGB8555_Premultiplied.jp2 b/autotests/write/format/jp2/Format_ARGB8555_Premultiplied.jp2 new file mode 100644 index 0000000..c539780 Binary files /dev/null and b/autotests/write/format/jp2/Format_ARGB8555_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_ARGB8565_Premultiplied.jp2 b/autotests/write/format/jp2/Format_ARGB8565_Premultiplied.jp2 new file mode 100644 index 0000000..75b8f5f Binary files /dev/null and b/autotests/write/format/jp2/Format_ARGB8565_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_BGR30.jp2 b/autotests/write/format/jp2/Format_BGR30.jp2 new file mode 100644 index 0000000..5b0a851 Binary files /dev/null and b/autotests/write/format/jp2/Format_BGR30.jp2 differ diff --git a/autotests/write/format/jp2/Format_BGR888.jp2 b/autotests/write/format/jp2/Format_BGR888.jp2 new file mode 100644 index 0000000..8d51214 Binary files /dev/null and b/autotests/write/format/jp2/Format_BGR888.jp2 differ diff --git a/autotests/write/format/jp2/Format_CMYK8888.jp2 b/autotests/write/format/jp2/Format_CMYK8888.jp2 new file mode 100644 index 0000000..9f88517 Binary files /dev/null and b/autotests/write/format/jp2/Format_CMYK8888.jp2 differ diff --git a/autotests/write/format/jp2/Format_Grayscale16.jp2 b/autotests/write/format/jp2/Format_Grayscale16.jp2 new file mode 100644 index 0000000..5e438c4 Binary files /dev/null and b/autotests/write/format/jp2/Format_Grayscale16.jp2 differ diff --git a/autotests/write/format/jp2/Format_Grayscale8.jp2 b/autotests/write/format/jp2/Format_Grayscale8.jp2 new file mode 100644 index 0000000..d0748bf Binary files /dev/null and b/autotests/write/format/jp2/Format_Grayscale8.jp2 differ diff --git a/autotests/write/format/jp2/Format_Indexed8.jp2 b/autotests/write/format/jp2/Format_Indexed8.jp2 new file mode 100644 index 0000000..5ed0306 Binary files /dev/null and b/autotests/write/format/jp2/Format_Indexed8.jp2 differ diff --git a/autotests/write/format/jp2/Format_Mono.jp2 b/autotests/write/format/jp2/Format_Mono.jp2 new file mode 100644 index 0000000..2be3787 Binary files /dev/null and b/autotests/write/format/jp2/Format_Mono.jp2 differ diff --git a/autotests/write/format/jp2/Format_MonoLSB.jp2 b/autotests/write/format/jp2/Format_MonoLSB.jp2 new file mode 100644 index 0000000..2be3787 Binary files /dev/null and b/autotests/write/format/jp2/Format_MonoLSB.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGB16.jp2 b/autotests/write/format/jp2/Format_RGB16.jp2 new file mode 100644 index 0000000..ce92224 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGB16.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGB30.jp2 b/autotests/write/format/jp2/Format_RGB30.jp2 new file mode 100644 index 0000000..5b0a851 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGB30.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGB32.jp2 b/autotests/write/format/jp2/Format_RGB32.jp2 new file mode 100644 index 0000000..8d51214 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGB32.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGB444.jp2 b/autotests/write/format/jp2/Format_RGB444.jp2 new file mode 100644 index 0000000..908342b Binary files /dev/null and b/autotests/write/format/jp2/Format_RGB444.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGB555.jp2 b/autotests/write/format/jp2/Format_RGB555.jp2 new file mode 100644 index 0000000..98d52cd Binary files /dev/null and b/autotests/write/format/jp2/Format_RGB555.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGB666.jp2 b/autotests/write/format/jp2/Format_RGB666.jp2 new file mode 100644 index 0000000..e20d033 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGB666.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGB888.jp2 b/autotests/write/format/jp2/Format_RGB888.jp2 new file mode 100644 index 0000000..8d51214 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGB888.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA16FPx4.jp2 b/autotests/write/format/jp2/Format_RGBA16FPx4.jp2 new file mode 100644 index 0000000..c2fa12d Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA16FPx4.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA16FPx4_Premultiplied.jp2 b/autotests/write/format/jp2/Format_RGBA16FPx4_Premultiplied.jp2 new file mode 100644 index 0000000..15783e7 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA16FPx4_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA32FPx4.jp2 b/autotests/write/format/jp2/Format_RGBA32FPx4.jp2 new file mode 100644 index 0000000..c6bd64c Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA32FPx4.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA32FPx4_Premultiplied.jp2 b/autotests/write/format/jp2/Format_RGBA32FPx4_Premultiplied.jp2 new file mode 100644 index 0000000..c6bd64c Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA32FPx4_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA64.jp2 b/autotests/write/format/jp2/Format_RGBA64.jp2 new file mode 100644 index 0000000..9215e4e Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA64.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA64_Premultiplied.jp2 b/autotests/write/format/jp2/Format_RGBA64_Premultiplied.jp2 new file mode 100644 index 0000000..c6bd64c Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA64_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA8888.jp2 b/autotests/write/format/jp2/Format_RGBA8888.jp2 new file mode 100644 index 0000000..7ec4354 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA8888.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBA8888_Premultiplied.jp2 b/autotests/write/format/jp2/Format_RGBA8888_Premultiplied.jp2 new file mode 100644 index 0000000..a75f69b Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBA8888_Premultiplied.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBX16FPx4.jp2 b/autotests/write/format/jp2/Format_RGBX16FPx4.jp2 new file mode 100644 index 0000000..fb0bdcf Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBX16FPx4.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBX32FPx4.jp2 b/autotests/write/format/jp2/Format_RGBX32FPx4.jp2 new file mode 100644 index 0000000..fe92603 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBX32FPx4.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBX64.jp2 b/autotests/write/format/jp2/Format_RGBX64.jp2 new file mode 100644 index 0000000..fe92603 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBX64.jp2 differ diff --git a/autotests/write/format/jp2/Format_RGBX8888.jp2 b/autotests/write/format/jp2/Format_RGBX8888.jp2 new file mode 100644 index 0000000..8d51214 Binary files /dev/null and b/autotests/write/format/jp2/Format_RGBX8888.jp2 differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index c6d8a47..bcc9170 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -86,6 +86,12 @@ if (LibJXL_FOUND AND LibJXLThreads_FOUND) endif() endif() +if (OpenJPEG_FOUND) + kimageformats_add_plugin(kimg_jp2 SOURCES jp2.cpp scanlineconverter.cpp) + target_include_directories(kimg_jp2 PRIVATE ${OPENJPEG_INCLUDE_DIRS}) + target_link_libraries(kimg_jp2 PRIVATE ${OPENJPEG_LIBRARIES}) +endif() + ################################## kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp) diff --git a/src/imageformats/jp2.cpp b/src/imageformats/jp2.cpp new file mode 100644 index 0000000..2524da4 --- /dev/null +++ b/src/imageformats/jp2.cpp @@ -0,0 +1,848 @@ +/* + 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_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_codec(nullptr) + , m_quality(-1) + , m_subtype(JP2_SUBTYPE) + { + + } + ~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; + } + + // 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 (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; + } + + bool imageToJp2(const QImage &image) + { + auto ncomp = image.hasAlphaChannel() ? 4 : 3; + auto prec = 8; + auto cs = OPJ_CLRSPC_SRGB; + auto convFormat = image.format(); + auto isFloat = false; + + 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 (strcmp(opj_version(), "2.5.3") >= 0) { + 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]; + } + } + } + + // With SRGB, Gray and CMYK, writing the colorspace gives an assert + if (cs == OPJ_CLRSPC_UNKNOWN || cs == OPJ_CLRSPC_UNSPECIFIED) { + 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; + + // 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" diff --git a/src/imageformats/jp2.json b/src/imageformats/jp2.json new file mode 100644 index 0000000..cf5898e --- /dev/null +++ b/src/imageformats/jp2.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "jp2", "j2k", "jpf" ], + "MimeTypes": [ "image/jp2" ] +} diff --git a/src/imageformats/jp2_p.h b/src/imageformats/jp2_p.h new file mode 100644 index 0000000..330de8a --- /dev/null +++ b/src/imageformats/jp2_p.h @@ -0,0 +1,44 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#ifndef KIMG_JP2_P_H +#define KIMG_JP2_P_H + +#include +#include + +class JP2HandlerPrivate; +class JP2Handler : public QImageIOHandler +{ +public: + JP2Handler(); + + bool canRead() const override; + bool read(QImage *image) override; + bool write(const QImage &image) override; + + bool supportsOption(QImageIOHandler::ImageOption option) const override; + void setOption(ImageOption option, const QVariant &value) override; + QVariant option(QImageIOHandler::ImageOption option) const override; + + static bool canRead(QIODevice *device); + +private: + const QScopedPointer d; +}; + +class JP2Plugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "jp2.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_JP2_P_H