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