From d70729cff0e151baec3c9333ca19a9360c84452f Mon Sep 17 00:00:00 2001 From: mio Date: Thu, 19 Sep 2024 14:13:21 +1000 Subject: [PATCH] Use OpenJPEG to read JP2 images. TDE currently uses an out-of-date Jasper for JPEG-2000 images. While updating to use a newer version of Jasper is possible, Debian and its derivated (i.e. Ubuntu, Devuan) don't package it. This means TDE is responsible for packaging Jasper and providing security updates. OpenJPEG on the other hand, is provided by all distrobutions that TDE officially supports, has a more stable API/ABI, and receives more frequent updates by various contributors. Currently, this doesn't have feature parity with the Jasper implementation. These limitations are listed in the 'jp2.cpp' file and should be addressed before this is pulled into a release branch. Signed-off-by: mio --- CMakeLists.txt | 20 +-- config.h.cmake | 4 +- kimgio/CMakeLists.txt | 7 +- kimgio/jp2.cpp | 323 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 331 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 41a4ed3ad..c2923f1c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,7 +105,7 @@ option( WITH_CUPS "Enable CUPS support" ON ) option( WITH_IMAGETOPS_BINARY "Enable installation of imagetops binary" ${WITH_ALL_OPTIONS} ) option( WITH_LUA "Enable LUA support" ${WITH_ALL_OPTIONS} ) option( WITH_TIFF "Enable tiff support" ${WITH_ALL_OPTIONS} ) -option( WITH_JASPER "Enable jasper (jpeg2k) support" ${WITH_ALL_OPTIONS} ) +option( WITH_OPENJPEG "Enable openjpeg (jpeg2k) support" ${WITH_ALL_OPTIONS} ) option( WITH_WEBP "Enable WebP support" ${WITH_ALL_OPTIONS} ) option( WITH_OPENEXR "Enable openexr support" ${WITH_ALL_OPTIONS} ) option( WITH_UTEMPTER "Use utempter for utmp management" ${WITH_ALL_OPTIONS} ) @@ -881,16 +881,16 @@ if( WITH_TIFF ) endif( WITH_TIFF ) -##### check for jasper ########################## - -if( WITH_JASPER ) - find_package( Jasper ) - if( NOT JASPER_FOUND ) - message(FATAL_ERROR "\njasper are requested, but not found on your system" ) - endif( NOT JASPER_FOUND ) - set( HAVE_JASPER 1 ) -endif( WITH_JASPER ) +##### check for openjpeg ########################## +if( WITH_OPENJPEG ) + pkg_search_module( OPENJPEG libopenjp2 ) + if( NOT OPENJPEG_FOUND ) + tde_message_fatal( "JPEG-2000 support requested, but openjpeg was not found on your system") + endif() + message( STATUS "JPEG-2000 support enabled" ) + set( HAVE_OPENJPEG 1 ) +endif( WITH_OPENJPEG ) ##### check for webp ############################ diff --git a/config.h.cmake b/config.h.cmake index fa6580cf3..bba918f12 100644 --- a/config.h.cmake +++ b/config.h.cmake @@ -300,8 +300,8 @@ /* Define to 1 if you have the header file. */ #cmakedefine HAVE_INTTYPES_H 1 -/* Define if you have jasper */ -#cmakedefine HAVE_JASPER 1 +/* Define if you have openjpeg */ +#cmakedefine HAVE_OPENJPEG 1 /* Defines if your system has the libart library */ #cmakedefine HAVE_LIBART 1 diff --git a/kimgio/CMakeLists.txt b/kimgio/CMakeLists.txt index cd013e588..80c20f7a6 100644 --- a/kimgio/CMakeLists.txt +++ b/kimgio/CMakeLists.txt @@ -16,6 +16,7 @@ include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/tdecore ${CMAKE_SOURCE_DIR}/tdecore + ${OPENJPEG_INCLUDE_DIRS} ) link_directories( @@ -75,11 +76,11 @@ tde_add_kpart( ${target} ##### kimg_jp2 ################################## -if( JASPER_FOUND ) +if( OPENJPEG_FOUND ) set( target kimg_jp2 ) tde_add_kpart( ${target} SOURCES jp2.cpp - LINK tdecore-shared ${JASPER_LIBRARIES} + LINK tdecore-shared ${OPENJPEG_LIBRARIES} DESTINATION ${PLUGIN_INSTALL_DIR} ) tde_create_translated_desktop( @@ -87,7 +88,7 @@ if( JASPER_FOUND ) DESTINATION ${SERVICES_INSTALL_DIR} PO_DIR mimetypes ) -endif( JASPER_FOUND ) +endif( OPENJPEG_FOUND ) ##### kimg_pcx ################################## diff --git a/kimgio/jp2.cpp b/kimgio/jp2.cpp index ff64f9263..948eef3fd 100644 --- a/kimgio/jp2.cpp +++ b/kimgio/jp2.cpp @@ -1,7 +1,8 @@ // This library is distributed under the conditions of the GNU LGPL. #include "config.h" -#ifdef HAVE_JASPER +#ifdef HAVE_OPENJPEG + #include #include "jp2.h" @@ -15,18 +16,322 @@ #ifdef HAVE_STDINT_H #include #endif + +#include + +#include #include #include #include #include #include +#include + +/* + * JPEG-2000 Plugin for KImageIO. + * + * Current limitations: + * - Only reads sRGB/Grayscale images (doesn't convert colorspace). + * - Doesn't support writing images. + * - Doesn't support OPJ_CODEC_J2K. + * - Doesn't support subsampling. + * - Doesn't read ICC profiles. + * - Doesn't write images. + * + * Improvements: + * - kimgio_jp2_read_srgba/kimgio_jp2_read_grayscale could be merged. + * + * The API documentation is rather poor, so good references on how to use OpenJPEG + * are the tools provided by OpenJPEG, such as 'opj_decompress': + * https://github.com/uclouvain/openjpeg/blob/master/src/bin/jp2/opj_decompress.c + * https://github.com/uclouvain/openjpeg/blob/master/src/bin/jp2/opj_compress.c + */ + +// kdDebug category +constexpr int kCategory = 399; + +struct KIMGJP2Wrapper +{ + public: + opj_codec_t *codec { nullptr }; + opj_image_t *image { nullptr }; + opj_stream_t *stream { nullptr }; + KTempFile tempFile; + + ~KIMGJP2Wrapper() + { + if (stream) + { + opj_stream_destroy(stream); + } + if (image) + { + opj_image_destroy(image); + } + if (codec) + { + opj_destroy_codec(codec); + } + } +}; + +static void kimgio_jp2_err_handler(const char *message, void *data) +{ + kdError(kCategory) << "Error decoding JP2 image: " << message; +} + +static void kimgio_jp2_info_handler(const char *message, void *data) +{ + // Reports status, e.g.: Main header has been correctly decoded. + // kdDebug(kCategory) << "JP2 decoding message: " << message; +} + +static void kimgio_jp2_warn_handler(const char *message, void *data) +{ + kdWarning(kCategory) << "Warning decoding JP2 image: " << message; +} + +static void kimgio_jp2_read_srgba(TQImage &image, const KIMGJP2Wrapper &jp2) +{ + const int height = image.height(); + const int width = image.width(); + + unsigned char alphaMask = 0x0; + + for (int row = 0; row < height; row++) + { + for (int col = 0; col < width; col++) + { + const int offset = row * width + col; + OPJ_INT32 r, g, b, a = 0xFF; + + // Convert to unsigned + r = jp2.image->comps[0].data[offset]; + r += (jp2.image->comps[0].sgnd ? 1 << (jp2.image->comps[0].prec - 1) : 0); + + g = jp2.image->comps[1].data[offset]; + g += (jp2.image->comps[0].sgnd ? 1 << (jp2.image->comps[1].prec - 1) : 0); + + b = jp2.image->comps[2].data[offset]; + b += (jp2.image->comps[2].sgnd ? 1 << (jp2.image->comps[2].prec - 1) : 0); + + if (jp2.image->numcomps > 3 && jp2.image->comps[3].alpha) + { + a = jp2.image->comps[3].data[offset]; + a += (jp2.image->comps[3].sgnd ? 1 << (jp2.image->comps[3].prec - 1) : 0); + } + + image.setPixel(col, row, tqRgba(r, g, b, a)); + alphaMask |= (255 - a); + } + } + + if (alphaMask != 0x0) + { + image.setAlphaBuffer(true); + } +} + +static void kimgio_jp2_read_grayscale(TQImage &image, const KIMGJP2Wrapper &jp2) +{ + const int height = image.height(); + const int width = image.width(); + + unsigned char alphaMask = 0x0; + + for (int row = 0; row < height; row++) + { + for (int col = 0; col < width; col++) + { + const int offset = row * width + col; + OPJ_INT32 g, a = 0xFF; + + // Convert to unsigned + g = jp2.image->comps[0].data[offset]; + g += (jp2.image->comps[0].sgnd ? (1 << jp2.image->comps[0].prec - 1) : 0); + + if (jp2.image->numcomps > 1 && jp2.image->comps[1].alpha) + { + a = jp2.image->comps[1].data[offset]; + a = (jp2.image->comps[1].sgnd ? (1 << jp2.image->comps[1].prec - 1) : 0); + } + + image.setPixel(col, row, tqRgba(g, g, g, a)); + alphaMask |= (255 - a); + } + } + + if (alphaMask != 0x0) + { + image.setAlphaBuffer(true); + } +} + +static const char *colorspaceToString(COLOR_SPACE clr) +{ + switch (clr) + { + case OPJ_CLRSPC_SRGB: + { + return "sRGB"; + } + case OPJ_CLRSPC_GRAY: + { + return "sRGB Grayscale"; + } + case OPJ_CLRSPC_SYCC: + { + return "YUV"; + } + case OPJ_CLRSPC_EYCC: + { + return "YCbCr"; + } + case OPJ_CLRSPC_CMYK: + { + return "CMYK"; + } + case OPJ_CLRSPC_UNSPECIFIED: + { + return "Unspecified"; + } + default: + { + return "Unknown"; + } + } +} -// dirty, but avoids a warning because jasper.h includes jas_config.h. -#undef PACKAGE -#undef VERSION -#include +TDE_EXPORT void kimgio_jp2_read(TQImageIO* io) +{ + KIMGJP2Wrapper jp2; + opj_dparameters_t parameters; + + if (auto tqfile = dynamic_cast(io->ioDevice())) + { + jp2.stream = opj_stream_create_default_file_stream(tqfile->name().local8Bit().data(), OPJ_TRUE); + } + else + { + // 4096 (=4k) is a common page size. + constexpr int pageSize = 4096; + + // Use a temporary file, since TQSocket::size() reports bytes + // available to read *now*, not the file size. + if (jp2.tempFile.status() != 0) + { + kdError(kCategory) << "Failed to create temporary file for non-TQFile IO" << endl; + return; + } + jp2.tempFile.setAutoDelete(true); + + TQFile *tempFile = jp2.tempFile.file(); + TQByteArray b(pageSize); + TQ_LONG bytesRead; + + // 0 or -1 is EOF / error + while ((bytesRead = io->ioDevice()->readBlock(b.data(), pageSize)) > 0) + { + if ((tempFile->writeBlock(b.data(), bytesRead)) == -1) + { + break; + } + } -// code taken in parts from JasPer's jiv.c + // flush everything out to disk + tempFile->flush(); + jp2.stream = opj_stream_create_default_file_stream(tempFile->name().local8Bit().data(), OPJ_TRUE); + } + + if (nullptr == jp2.stream) + { + kdError(kCategory) << "Failed to create input stream for JP2" << endl; + io->setStatus(IO_ResourceError); + return; + } + + jp2.codec = opj_create_decompress(OPJ_CODEC_JP2); + if (nullptr == jp2.codec) + { + kdError(kCategory) << "Unable to create decompressor for JP2" << endl; + io->setStatus(IO_ResourceError); + return; + } + opj_set_error_handler(jp2.codec, kimgio_jp2_err_handler, nullptr); + opj_set_info_handler(jp2.codec, kimgio_jp2_info_handler, nullptr); + opj_set_warning_handler(jp2.codec, kimgio_jp2_warn_handler, nullptr); + + opj_set_default_decoder_parameters(¶meters); + if (OPJ_FALSE == opj_setup_decoder(jp2.codec, ¶meters)) + { + kdError(kCategory) << "Failed to setup decoder for JP2" << endl; + io->setStatus(IO_ResourceError); + return; + } + + if (OPJ_FALSE == opj_read_header(jp2.stream, jp2.codec, &jp2.image)) + { + kdError(kCategory) << "Failed to read JP2 header" << endl; + io->setStatus(IO_ReadError); + return; + } + + if (OPJ_FALSE == opj_decode(jp2.codec, jp2.stream, jp2.image)) + { + kdError(kCategory) << "Failed to decode JP2 image" << endl; + io->setStatus(IO_ReadError); + return; + } + + if (OPJ_FALSE == opj_end_decompress(jp2.codec, jp2.stream)) + { + kdError(kCategory) << "Failed to decode JP2 image ending" << endl; + io->setStatus(IO_ReadError); + return; + } + + OPJ_UINT32 width = jp2.image->x1 - jp2.image->x0; + OPJ_UINT32 height = jp2.image->y1 - jp2.image->y0; + TQImage image(width, height, 32); + + switch (jp2.image->color_space) + { + case OPJ_CLRSPC_SRGB: + { + kimgio_jp2_read_srgba(image, jp2); + break; + } + case OPJ_CLRSPC_GRAY: + { + kimgio_jp2_read_grayscale(image, jp2); + break; + } + default: + { + kdError(kCategory) << "Unsupported colorspace detected: " + << colorspaceToString(jp2.image->color_space) + << endl; + io->setStatus(IO_ReadError); + return; + } + } + + io->setImage(image); + io->setStatus(IO_Ok); +} + + +static void kimgio_jp2_write_handler(void *buffer, OPJ_SIZE_T buffer_size, void *user_data) +{ + // TODO(mio): +} + +TDE_EXPORT void kimgio_jp2_write(TQImageIO *io) +{ + kdDebug(kCategory) << "Writing JP2 with OpenJPEG is not supported yet." << endl; +} + +/* #define DEFAULT_RATE 0.10 #define MAXCMPTS 256 @@ -192,7 +497,7 @@ create_image( const TQImage& qi ) cmptparms[i].sgnd = false; } - jas_image_t* ji = jas_image_create( 3 /* number components */, cmptparms, JAS_CLRSPC_UNKNOWN ); + jas_image_t* ji = jas_image_create( 3 /* number components *//*, cmptparms, JAS_CLRSPC_UNKNOWN ); delete[] cmptparms; // returning 0 is ok @@ -324,5 +629,7 @@ kimgio_jp2_write( TQImageIO* io ) io->setStatus( IO_Ok ); } // kimgio_jp2_write -#endif // HAVE_JASPER +*/ + +#endif // HAVE_OPENJPEG