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 <stigma@disroot.org>
issue/51/jasper-to-openjpeg
mio 2 months ago
parent 5c1b76e6bf
commit d70729cff0

@ -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_IMAGETOPS_BINARY "Enable installation of imagetops binary" ${WITH_ALL_OPTIONS} )
option( WITH_LUA "Enable LUA support" ${WITH_ALL_OPTIONS} ) option( WITH_LUA "Enable LUA support" ${WITH_ALL_OPTIONS} )
option( WITH_TIFF "Enable tiff 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_WEBP "Enable WebP support" ${WITH_ALL_OPTIONS} )
option( WITH_OPENEXR "Enable openexr support" ${WITH_ALL_OPTIONS} ) option( WITH_OPENEXR "Enable openexr support" ${WITH_ALL_OPTIONS} )
option( WITH_UTEMPTER "Use utempter for utmp management" ${WITH_ALL_OPTIONS} ) option( WITH_UTEMPTER "Use utempter for utmp management" ${WITH_ALL_OPTIONS} )
@ -881,16 +881,16 @@ if( WITH_TIFF )
endif( WITH_TIFF ) endif( WITH_TIFF )
##### check for jasper ########################## ##### check for openjpeg ##########################
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 )
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 ############################ ##### check for webp ############################

@ -300,8 +300,8 @@
/* Define to 1 if you have the <inttypes.h> header file. */ /* Define to 1 if you have the <inttypes.h> header file. */
#cmakedefine HAVE_INTTYPES_H 1 #cmakedefine HAVE_INTTYPES_H 1
/* Define if you have jasper */ /* Define if you have openjpeg */
#cmakedefine HAVE_JASPER 1 #cmakedefine HAVE_OPENJPEG 1
/* Defines if your system has the libart library */ /* Defines if your system has the libart library */
#cmakedefine HAVE_LIBART 1 #cmakedefine HAVE_LIBART 1

@ -16,6 +16,7 @@ include_directories(
${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}
${CMAKE_BINARY_DIR}/tdecore ${CMAKE_BINARY_DIR}/tdecore
${CMAKE_SOURCE_DIR}/tdecore ${CMAKE_SOURCE_DIR}/tdecore
${OPENJPEG_INCLUDE_DIRS}
) )
link_directories( link_directories(
@ -75,11 +76,11 @@ tde_add_kpart( ${target}
##### kimg_jp2 ################################## ##### kimg_jp2 ##################################
if( JASPER_FOUND ) if( OPENJPEG_FOUND )
set( target kimg_jp2 ) set( target kimg_jp2 )
tde_add_kpart( ${target} tde_add_kpart( ${target}
SOURCES jp2.cpp SOURCES jp2.cpp
LINK tdecore-shared ${JASPER_LIBRARIES} LINK tdecore-shared ${OPENJPEG_LIBRARIES}
DESTINATION ${PLUGIN_INSTALL_DIR} DESTINATION ${PLUGIN_INSTALL_DIR}
) )
tde_create_translated_desktop( tde_create_translated_desktop(
@ -87,7 +88,7 @@ if( JASPER_FOUND )
DESTINATION ${SERVICES_INSTALL_DIR} DESTINATION ${SERVICES_INSTALL_DIR}
PO_DIR mimetypes PO_DIR mimetypes
) )
endif( JASPER_FOUND ) endif( OPENJPEG_FOUND )
##### kimg_pcx ################################## ##### kimg_pcx ##################################

@ -1,7 +1,8 @@
// This library is distributed under the conditions of the GNU LGPL. // This library is distributed under the conditions of the GNU LGPL.
#include "config.h" #include "config.h"
#ifdef HAVE_JASPER #ifdef HAVE_OPENJPEG
#include <unistd.h> #include <unistd.h>
#include "jp2.h" #include "jp2.h"
@ -15,18 +16,322 @@
#ifdef HAVE_STDINT_H #ifdef HAVE_STDINT_H
#include <stdint.h> #include <stdint.h>
#endif #endif
#include <openjpeg.h>
#include <kdebug.h>
#include <tdetempfile.h> #include <tdetempfile.h>
#include <tqcolor.h> #include <tqcolor.h>
#include <tqcstring.h> #include <tqcstring.h>
#include <tqfile.h> #include <tqfile.h>
#include <tqimage.h> #include <tqimage.h>
#include <tqmemarray.h>
/*
* 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. TDE_EXPORT void kimgio_jp2_read(TQImageIO* io)
#undef PACKAGE {
#undef VERSION KIMGJP2Wrapper jp2;
#include <jasper/jasper.h> opj_dparameters_t parameters;
if (auto tqfile = dynamic_cast<TQFile *>(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(&parameters);
if (OPJ_FALSE == opj_setup_decoder(jp2.codec, &parameters))
{
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 DEFAULT_RATE 0.10
#define MAXCMPTS 256 #define MAXCMPTS 256
@ -192,7 +497,7 @@ create_image( const TQImage& qi )
cmptparms[i].sgnd = false; 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; delete[] cmptparms;
// returning 0 is ok // returning 0 is ok
@ -324,5 +629,7 @@ kimgio_jp2_write( TQImageIO* io )
io->setStatus( IO_Ok ); io->setStatus( IO_Ok );
} // kimgio_jp2_write } // kimgio_jp2_write
#endif // HAVE_JASPER */
#endif // HAVE_OPENJPEG

Loading…
Cancel
Save