diff --git a/README b/README index 98fb2532..48c8be4c 100644 --- a/README +++ b/README @@ -57,7 +57,7 @@ See COPYING.FCGI for licensing information for these libraries. REQUIREMENTS ------------ Requirements: libtiff, zlib and the IJG JPEG development libraries. -Optional: libmemcached (for Memcached) and Kakadu (for JPEG2000) +Optional: libmemcached (for Memcached) and Kakadu or OpenJPEG (for JPEG2000) Plus, of course, an fcgi-enabled web server. The server has been successfully tested on the following servers: @@ -115,6 +115,19 @@ following parameters to the ./configure command +OPTIONAL LIBRARIES: OPENJPEG +---------------------------- +IIPImage is able to decode JPEG2000 images via the free software OpenJPEG +(https://github.com/uclouvain/openjpeg). This is still much slower than +decoding via the Kakadu SDK, and it is also highly experimental. + +It is also possible to use both the Kakadu SDK and OpenJPEG. +Then the Kakadu SDK will be used by default. +OpenJPEG can be enabled by setting the environment variable USE_OPENJPEG +to any value larger than 0, for example with USE_OPENJPEG=1. + + + INSTALLATION ------------ Simply copy the executable called iipsrv.fcgi in the src subdirectory into diff --git a/configure.ac b/configure.ac index 4f87bb36..dba86c10 100644 --- a/configure.ac +++ b/configure.ac @@ -205,6 +205,44 @@ AM_CONDITIONAL( [ENABLE_MODULES], [test x$modules = xtrue] ) AC_SUBST(DL_LIBS) +#************************************************************ + +# Check for OpenJPEG JPEG2000 library + +AC_ARG_WITH( openjpeg, + [ --with-openjpeg=DIR location of the openjpeg source files], + openjpeg_path=$withval) + + +AC_CHECK_FILE($openjpeg_path/src/lib/openjp2/openjpeg.h, + AC_MSG_RESULT([configure: Found OpenJPEG sources. Will compile JPEG2000 support]); OPENJPEG=true, + AC_MSG_RESULT([configure: No OpenJPEG JPEG2000 Sources Found]); OPENJPEG=false +) + +if test "x$OPENJPEG" = xtrue; then + INCLUDES="$INCLUDES -I$openjpeg_path/src/lib/openjp2/" + LIBS="$LIBS $openjpeg_path/bin/libopenjp2.so -lpthread" +else + PKG_CHECK_MODULES([OPENJPEG], [libopenjp2], [OPENJPEG=true], [OPENJPEG=false]) + if test "x$OPENJPEG" = xtrue; then + INCLUDES="$INCLUDES $OPENJPEG_CFLAGS" + LIBS="$LIBS $OPENJPEG_LIBS" + fi +fi + +if test "x$OPENJPEG" = xtrue; then + AC_DEFINE(HAVE_OPENJPEG) + EXTRAS="OpenJPEGImage.o" +fi + +AM_CONDITIONAL([ENABLE_OPENJPEG], [test x$OPENJPEG = xtrue]) + +AC_SUBST(INCLUDES) +AC_SUBST(EXTRAS) + +AC_SUBST(LIBS) + + #************************************************************ # Check for Kakadu JPEG2000 library @@ -346,6 +384,7 @@ Options Enabled: --------------- Memcached: ${MEMCACHED} JPEG2000 (Kakadu): ${KAKADU} + JPEG2000 (OpenJPEG): ${OPENJPEG} ]) # PNG Output: ${PNG} diff --git a/src/Environment.h b/src/Environment.h index 8144d7c9..ac40606d 100644 --- a/src/Environment.h +++ b/src/Environment.h @@ -129,6 +129,15 @@ class Environment { return layers; } + static bool getUseOpenJPEG(){ + char* envpara = getenv( "USE_OPENJPEG" ); + int value; + if( envpara ) value = atoi( envpara ); + else value = 0; + + return (value > 0); + } + static std::string getFileSystemPrefix(){ char* envpara = getenv( "FILESYSTEM_PREFIX" ); diff --git a/src/FIF.cc b/src/FIF.cc index d35b8f4a..1e61dc33 100644 --- a/src/FIF.cc +++ b/src/FIF.cc @@ -29,6 +29,10 @@ #include "KakaduImage.h" #endif +#ifdef HAVE_OPENJPEG +#include "OpenJPEGImage.h" +#endif + #define MAXIMAGECACHE 1000 // Max number of items in image cache @@ -120,10 +124,20 @@ void FIF::run( Session* session, const string& src ){ if( session->loglevel >= 2 ) *(session->logfile) << "FIF :: TIFF image detected" << endl; *session->image = new TPTImage( test ); } -#ifdef HAVE_KAKADU +#if defined(HAVE_KAKADU) || defined(HAVE_OPENJPEG) else if( format == JPEG2000 ){ - if( session->loglevel >= 2 ) *(session->logfile) << "FIF :: JPEG2000 image detected" << endl; - *session->image = new KakaduImage( test ); + if( session->loglevel >= 2 ) + *(session->logfile) << "FIF :: JPEG2000 image detected" << endl; +#if defined(HAVE_OPENJPEG) + if( session->useOpenJPEG ) + *session->image = new OpenJPEGImage( test ); +#endif +#if defined(HAVE_KAKADU) +#if defined(HAVE_OPENJPEG) + else +#endif + *session->image = new KakaduImage( test ); +#endif } #endif else throw string( "Unsupported image type: " + argument ); @@ -145,7 +159,7 @@ void FIF::run( Session* session, const string& src ){ throw string( "Unsupported image type: " + imtype ); } else{ - // Construct our dynamic loading image decoder + // Construct our dynamic loading image decoder session->image = new DSOImage( test ); (*session->image)->Load( (*mod_it).second ); diff --git a/src/Main.cc b/src/Main.cc index c159327e..c46e52a9 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -256,6 +256,15 @@ int main( int argc, char *argv[] ) string filesystem_prefix = Environment::getFileSystemPrefix(); +#if !defined(HAVE_OPENJPEG) + bool useOpenJPEG = false; +#elif defined(HAVE_KAKADU) + bool useOpenJPEG = Environment::getUseOpenJPEG(); +#else + bool useOpenJPEG = true; +#endif + + // Set up our watermark object Watermark watermark( Environment::getWatermark(), Environment::getWatermarkOpacity(), @@ -290,7 +299,10 @@ int main( int argc, char *argv[] ) else logfile << max_layers << endl; } #ifdef HAVE_KAKADU - logfile << "Setting up JPEG2000 support via Kakadu SDK" << endl; + if(!useOpenJPEG) logfile << "Setting up JPEG2000 support via Kakadu SDK" << endl; +#endif +#ifdef HAVE_OPENJPEG + if(useOpenJPEG) logfile << "Setting up JPEG2000 support via OpenJPEG" << endl; #endif } @@ -473,6 +485,7 @@ int main( int argc, char *argv[] ) session.out = &writer; session.watermark = &watermark; session.headers.clear(); + session.useOpenJPEG = useOpenJPEG; char* header = NULL; diff --git a/src/Makefile.am b/src/Makefile.am index c21c3d4b..e1516761 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -13,6 +13,10 @@ if ENABLE_KAKADU iipsrv_fcgi_LDADD += KakaduImage.o endif +if ENABLE_OPENJPEG +iipsrv_fcgi_LDADD += OpenJPEGImage.o +endif + #if ENABLE_PNG #iipsrv_fcgi_LDADD += PNGCompressor.o PTL.o #endif @@ -21,7 +25,7 @@ if ENABLE_MODULES iipsrv_fcgi_LDADD += DSOImage.o endif -EXTRA_iipsrv_fcgi_SOURCES = DSOImage.h DSOImage.cc KakaduImage.h KakaduImage.cc Main.cc +EXTRA_iipsrv_fcgi_SOURCES = DSOImage.h DSOImage.cc KakaduImage.h KakaduImage.cc Main.cc OpenJPEGImage.h OpenJPEGImage.cc iipsrv_fcgi_SOURCES = \ IIPImage.h \ diff --git a/src/OpenJPEGImage.cc b/src/OpenJPEGImage.cc new file mode 100644 index 00000000..c453488b --- /dev/null +++ b/src/OpenJPEGImage.cc @@ -0,0 +1,538 @@ +/* IIP Server: OpenJPEG JPEG2000 handler + + Copyright (C) 2015 Moravian Library in Brno (http://www.mzk.cz/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +//#define DEBUG 1 + +#include "OpenJPEGImage.h" + +#include +#include +#include +#include + +#include + +using namespace std; + +/************************************************************************/ +/* callbacks */ +/************************************************************************/ +// These are called by OpenJPEG library. +// This driver then decides to ignore the messages or to process them. +// While not in debug mode errors are processed but warnings and info messages are omitted + +static void error_callback(const char* msg, void* /*client_data*/) +{ + stringstream ss; + ss << "ERROR :: OpenJPEG core :: " << msg; + throw file_error(ss.str()); +} + +static void warning_callback(const char* msg, void* /*client_data*/) +{ +#ifdef DEBUG + logfile << "WARNING :: OpenJPEG core :: " << msg << flush; +#endif +} + +static void info_callback(const char* msg, void* /*client_data*/) +{ +#ifdef DEBUG + logfile << "INFO :: OpenJPEG core :: " << msg << flush; +#endif +} + +/************************************************************************/ +/* openImage() */ +/************************************************************************/ +// Opens file with image and calls loadImageInfo() + +void OpenJPEGImage::openImage() throw(file_error) +{ +#ifdef DEBUG + Timer timer; + timer.start(); + logfile << "INFO :: OpenJPEG :: openImage() :: started" << endl + << flush; +#endif + + filename = getFileName(currentX, currentY); // Get file name + updateTimestamp(filename); // Check if our image has been modified + + loadImageInfo(currentX, currentY); + isSet = true; // Image is opened and info is set + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: openImage() :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif +} + +/************************************************************************/ +/* closeImage() */ +/************************************************************************/ +// Closes file with image + +void OpenJPEGImage::closeImage() +{ +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: closeImage() :: started" << endl + << flush; +#endif + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: closeImage() :: ended" << endl + << flush; +#endif +} + +/************************************************************************/ +/* loadImageInfo() */ +/************************************************************************/ +// Saves important image information to IIPImage and OpenJPEGImage variables + +void OpenJPEGImage::loadImageInfo(int /*seq*/, int /*ang*/) throw(file_error) +{ +#ifdef DEBUG + Timer timer; + timer.start(); + logfile << "INFO :: OpenJPEG :: loadImageInfo() :: started" << endl + << flush; +#endif + + static opj_image_t* l_image; // Image structure + static opj_stream_t* l_stream; // File stream + static opj_codec_t* l_codec; // Handle to a decompressor + + class Finally { + // This class makes sure that the resources are deallocated properly + public: + ~Finally() + { + opj_end_decompress(l_codec, l_stream); + opj_stream_destroy(l_stream); + opj_destroy_codec(l_codec); + opj_image_destroy(l_image); + l_codec = NULL; + l_stream = NULL; + l_image = NULL; + } + }; + Finally finally; // Allocated on stack, destructor is called on both successful and exceptional scope exit + + l_codec = opj_create_decompress(OPJ_CODEC_JP2); // Create decompress codec + + // Set callback handlers. OPJ library then passes information, warnings and errors to specified methods. + opj_set_info_handler(l_codec, info_callback, 00); + opj_set_warning_handler(l_codec, warning_callback, 00); + opj_set_error_handler(l_codec, error_callback, 00); + + opj_dparameters_t parameters; // Set default decoder parameters + opj_set_default_decoder_parameters(¶meters); + if (!opj_setup_decoder(l_codec, ¶meters)) { + throw file_error("ERROR :: OpenJPEG :: openImage() :: opj_setup_decoder() failed"); // Setup decoder + } + + if (!(l_stream = opj_stream_create_default_file_stream(filename.c_str(), 1))) { + throw file_error("ERROR :: OpenJPEG :: openImage() :: opj_stream_create_default_file_stream() failed"); // Create stream + } + + if (!opj_read_header(l_stream, l_codec, &l_image)) { + throw file_error("ERROR :: OpenJPEG :: openImage() :: opj_read_header() failed"); // Read main header + } + + opj_codestream_info_v2_t* cst_info = opj_get_cstr_info(l_codec); // Get info structure + image_tile_width = cst_info->tdx; // Save image tile width - tile width that this image operates with + image_tile_height = cst_info->tdy; // Save image tile height + numResolutions = cst_info->m_default_tile_info.tccp_info[0].numresolutions; // Save number of resolution levels in image + max_layers = cst_info->m_default_tile_info.numlayers; // Save number of layers +#ifdef DEBUG + logfile << "OpenJPEG :: " << max_layers << " quality layers detected" << endl + << flush; +#endif + opj_destroy_cstr_info(&cst_info); // We already read everything we needed from info structure + + // Check whether image parameters make sense + if (l_image->x1 <= l_image->x0 || l_image->y1 <= l_image->y0 || + l_image->numcomps == 0 || + l_image->comps[0].w != l_image->x1 - l_image->x0 || + l_image->comps[0].h != l_image->y1 - l_image->y0) { + throw file_error("ERROR :: OpenJPEG :: loadImageInfo() :: Could not handle that image"); + } + + // Check for 420 format + if (l_image->color_space != OPJ_CLRSPC_SRGB && l_image->numcomps == 3 && + l_image->comps[1].w == l_image->comps[0].w / 2 && + l_image->comps[1].h == l_image->comps[0].h / 2 && + l_image->comps[2].w == l_image->comps[0].w / 2 && + l_image->comps[2].h == l_image->comps[0].h / 2) { + throw file_error("ERROR :: OpenJPEG :: loadImageInfo() :: 420 format detected."); + } + + // Check whether component parameters make sense + for (int i = 2; i <= (int)l_image->numcomps; ++i) { + if (l_image->comps[i - 1].w != l_image->comps[0].w || l_image->comps[i - 1].h != l_image->comps[0].h) { + throw file_error("ERROR :: OpenJPEG :: loadImageInfo() :: Could not handle that image"); + } + } + + // Save color space + switch ((channels = l_image->numcomps)) { + case 3: + colourspace = sRGB; + break; + case 1: + colourspace = GREYSCALE; + break; + default: + throw file_error("ERROR :: OpenJPEG :: Unsupported color space"); + } + + bpc = l_image->comps[0].prec; // Save bit depth + sgnd = l_image->comps[0].sgnd; // Save whether the data are signed + + // Save first resolution level + image_widths.push_back((raster_width = l_image->x1 - l_image->x0)); + image_heights.push_back((raster_height = l_image->y1 - l_image->y0)); + + unsigned int tmp_w = raster_width; + unsigned int tmp_h = raster_height; +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: Resolution : " << tmp_w << "x" << tmp_h << endl + << flush; +#endif + unsigned short i = 1; + // Save other resolution levels + for (; tmp_w > tile_width || tmp_h > tile_height; ++i) { + image_widths.push_back((tmp_w /= 2)); + image_heights.push_back((tmp_h /= 2)); +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: Resolution : " << tmp_w << "x" << tmp_h << endl + << flush; +#endif + } + + // Generate virtual resolutions if necessary + if (i > numResolutions) { + virtual_levels = i - numResolutions; +#ifdef DEBUG + logfile << "WARNING :: OpenJPEG :: Insufficient resolution levels in JPEG2000 stream. Will generate " << virtual_levels << " extra levels dynamically." << endl; +#endif + } + + numResolutions = i; // Set number of resolutions to (number of picture resolutions + number of virtual resolutions) + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: loadImageInfo() :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif +} + +/************************************************************************/ +/* getTile() - Get an individual tile */ +/************************************************************************/ +// Get one individual tile from the opened picture + +RawTile OpenJPEGImage::getTile(int seq, int ang, unsigned int res, int layers, + unsigned int tile) throw(file_error) +{ +#ifdef DEBUG + Timer timer; + timer.start(); + logfile << "INFO :: OpenJPEG :: getTile() :: started" << endl + << flush; +#endif + + // Check whether requested resolution exists in the picture. + if (res > numResolutions) { + throw file_error("ERROR :: OpenJPEG :: getTile() :: Asked for non-existent resolution"); + } + + // Reverse the resolution number - resolutions in IIPImage are stored in reversed order + int vipsres = (numResolutions - 1) - res; + + unsigned int rem_x = image_widths[vipsres] % tile_width; // Get the width and height for last row and column tiles + unsigned int rem_y = image_heights[vipsres] % tile_height; // that means remaining pixels, which do not form a whole tile + + unsigned int ntlx = (image_widths[vipsres] / tile_width) + (rem_x == 0 ? 0 : 1); // Calculate the number of tiles in each direction + unsigned int ntly = (image_heights[vipsres] / tile_height) + (rem_y == 0 ? 0 : 1); + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: getTile() :: asked for: " << endl + << "\tTile width: " << tile_width << endl + << "\tTile height: " << tile_height << endl + << "\tResolution: " << res << ", meaning " << vipsres << " for OpenJPEG driver" << endl + << "\tResolution: " << image_widths[vipsres] << "x" << image_heights[vipsres] << endl + << "\tTile index: " << tile << endl + << "\tTiles available: " << ntlx << "x" << ntly << endl + << "\tRemaining tile width in last column: " << rem_x << endl + << "\tRemaining tile height in bottom row: " << rem_y << endl + << flush; +#endif + + // Check whether requested tile exists. + if (tile >= ntlx * ntly) { + throw file_error("ERROR :: OpenJPEG :: getTile() :: Asked for non-existent tile"); + } + + unsigned int tw = tile_width, th = tile_height; + + // Alter the tile size if it's in the last column + if ((tile % ntlx == ntlx - 1) && rem_x != 0) { + tw = rem_x; + } + + // Alter the tile size if it's in the bottom row + if ((tile / ntlx == ntly - 1) && rem_y != 0) { + th = rem_y; + } + + // Calculate the pixel offsets for this tile + int xoffset = (tile % ntlx) * tile_width; + int yoffset = (tile / ntlx) * tile_height; + +#ifdef DEBUG + logfile << "\tFinal tile size requested: " << tw << "x" << th << " @" << channels << endl + << flush; +#endif + + // Create Rawtile object and initialize it + RawTile rawtile(tile, res, seq, ang, tw, th, channels, 8); + rawtile.data = new unsigned char[tw * th * channels]; + rawtile.dataLength = tw * th * channels; + rawtile.filename = getImagePath(); + rawtile.timestamp = timestamp; + + // Set the number of layers to half of the number of detected layers if we have not set the layers parameter manually + if (layers <= 0) { + layers = ceil(max_layers / 2.0); + } + + // Process the tile - save data to rawfile.data + // We can decode a single tile only if requested size equals tile size defined in opened image + // If tile sizes defined in IIPImage and opened image do not match, indexes of tiles we ask OPJ library for do not match either + // In case of this tile size inconsistency, seventh parameter becomes -1. This means that OpenJPEG library will process the request as + // a request for region, not a tile. OPJ library will then select tiles that need to be decoded in order to decode requested region. + process(tw, th, xoffset, yoffset, res, layers, + (image_tile_width == tw && image_tile_height == th) ? tile : -1, rawtile.data); + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: getTile() :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif + + return rawtile; +} + +/************************************************************************/ +/* getRegion() - Get an entire region and not just a tile */ +/************************************************************************/ +// Gets selected region from opened picture + +void OpenJPEGImage::getRegion(int /*seq*/, int /*ang*/, unsigned int res, + int layers, int x, int y, + unsigned int w, unsigned int h, + unsigned char* buf) throw(file_error) +{ +#ifdef DEBUG + Timer timer; + timer.start(); + logfile << "INFO :: OpenJPEG :: getRegion() :: started" << endl + << flush; +#endif + + // Check if desired resolution exists + if (res > numResolutions) { + throw file_error("ERROR :: OpenJPEG :: getRegion() :: Asked for non-existent resolution"); + } + + // Check layer request + if (layers <= 0) { + layers = ceil(max_layers / 2.0); + } + + // Reverse resolution level number + int vipsres = (numResolutions - 1) - res; + + // Check if specified region is valid for this picture + if ((x + w) > image_widths[vipsres] || (y + h) > image_heights[vipsres]) { + throw file_error("ERROR :: OpenJPEG :: getRegion() :: Asked for region out of raster size"); + } + + // Get the region + process(w, h, x, y, res, layers, -1, buf); + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: getRegion() :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif +} + +/************************************************************************/ +/* process() - Main processing function */ +/************************************************************************/ +// Core method for recovering tiles and regions from opened picture via OPJ library + +void OpenJPEGImage::process(unsigned int tw, unsigned int th, + unsigned int xoffset, unsigned int yoffset, + unsigned int res, int layers, int tile, + void* d) throw(file_error) +{ + static opj_image_t* out_image; // Decoded image + static opj_stream_t* l_stream; // File stream + static opj_codec_t* l_codec; // Handle to a decompressor + + unsigned int factor = 1; // Downsampling factor - set it to default value + int vipsres = (numResolutions - 1) - res; // Reverse resolution number + + if (res < virtual_levels) { + // Handle virtual resolutions + // Once we enter this scope it means we need to provide a virtual resolution + // Factor 2 means half of the smallest original resolution, factor 4 means quarter of it and so on... + // Can not be negative or zero - see above condition. Is always >= 2 + factor = 2 * (virtual_levels - res); + // Take care of offsets as well + xoffset *= factor; + yoffset *= factor; + // We need to decode bigger region than requested. We are going to downsample it later, thus gain the desired size again + tw *= factor; + th *= factor; + // Set our resolution level back to the smallest original resolution + vipsres = numResolutions - 1 - virtual_levels; + } + + class Finally { + // This class makes sure that the resources are deallocated properly + public: + ~Finally() + { + opj_end_decompress(l_codec, l_stream); + opj_stream_destroy(l_stream); + opj_destroy_codec(l_codec); + opj_image_destroy(out_image); + l_codec = NULL; + l_stream = NULL; + out_image = NULL; + } + }; + // Allocated on stack, destructor is called on both successful and exceptional scope exit + Finally finally; + + l_codec = opj_create_decompress(OPJ_CODEC_JP2); // Create decompress codec + opj_set_info_handler(l_codec, info_callback, 00); // Set callback handlers + opj_set_warning_handler(l_codec, warning_callback, 00); + opj_set_error_handler(l_codec, error_callback, 00); + + if (!(l_stream = opj_stream_create_default_file_stream(filename.c_str(), 1))) { + // Create stream + throw file_error("ERROR :: OpenJPEG :: process() :: opj_stream_create_default_file_stream() failed"); + } + + opj_dparameters_t params; + params.cp_layer = layers; // Set quality layers + params.cp_reduce = 0; + if (!opj_setup_decoder(l_codec, ¶ms)) { + // Setup layers + throw file_error("ERROR :: OpenJPEG :: process() :: opj_setup_decoder() failed"); + } + + if (!opj_read_header(l_stream, l_codec, &out_image)) { + // Read main header + throw file_error("ERROR :: OpenJPEG :: process() :: opj_read_header() failed"); + } + if (!opj_set_decoded_resolution_factor(l_codec, vipsres)) { + // Setup resolution + throw file_error("ERROR :: OpenJPEG :: process() :: opj_set_decoded_resolution_factor() failed"); + } +#ifdef DEBUG + Timer timer; + timer.start(); + logfile << "INFO :: OpenJPEG :: process() :: Decoding started" << endl + << flush; +#endif + if (tile < 0) { + // In this scope we decode a region - OpenJPEG library selects tiles that need to be decoded itself +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: process() :: Decoding a region (not a single tile)" << endl + << flush; +#endif + // Tell OpenJPEG what region we want to decode + if (!opj_set_decode_area(l_codec, out_image, xoffset, yoffset, xoffset + tw, yoffset + th)) { + throw file_error("ERROR :: OpenJPEG :: process() :: opj_set_decode_area() failed"); + } + // Decode region from image + if (!opj_decode(l_codec, l_stream, out_image)) { + throw file_error("ERROR :: OpenJPEG :: process() :: opj_decode() failed"); + } + } + // Get a single tile if possible + else if (!opj_get_decoded_tile(l_codec, l_stream, out_image, tile)) { + throw file_error("ERROR :: OpenJPEG :: process() :: opj_get_decoded_tile() failed"); + } + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: process() :: Decoding took " << timer.getTime() << " microseconds" << endl + << "INFO :: OpenJPEG :: process() :: Decoded image info: " << endl + << "\tPrecision: " << out_image->comps[0].prec << endl + << "\tBPP: " << out_image->comps[0].bpc << endl + << "\tSigned: " << out_image->comps[0].sgnd << endl + << "\tXOFF: " << out_image->comps[0].x0 << endl + << "\tYOFF: " << out_image->comps[0].y0 << endl + << "\tXSIZE: " << out_image->comps[0].w << endl + << "\tYSIZE: " << out_image->comps[0].h << endl + << "\tRESNO: " << out_image->comps[0].resno_decoded << endl + << "INFO :: OpenJPEG :: process() :: Copying image data started" << endl + << flush; + timer.start(); +#endif + + // Now we need to pass decoded data to the buffer we received as a parameter (actually we received just a reference to it) + // Create new pointer to the buffer as unsigned char pointer + unsigned char* p_buffer = (unsigned char*)d; + // We are unable to perform pointer arithmetics on void pointers + unsigned int buffer_write_pos = 0; // Write position indicator + unsigned int h_pos = 0; + unsigned int w_pos; + unsigned int xy_position = 0; // Another position indicators + unsigned int color_comp; // Current color component indicator + for (; h_pos < th; h_pos += factor) { + // Vertical cycle - takes care of pixel columns + for (w_pos = 0; w_pos < tw; w_pos += factor) { + // Horizontal cycle - takes care of pixel rows + xy_position = (tw * h_pos) + w_pos; // Calculate exact position (pixel index) + for (color_comp = 0; color_comp < channels; ++color_comp) { + // For each color component in that pixel + // Get component data from opj_image structure which contains decoded tile/region + OPJ_INT32 int_data = out_image->comps[color_comp].data[xy_position]; + // Copy first byte from the integer we saved to buffer + p_buffer[buffer_write_pos + color_comp] = int_data & 0x000000ff; + //(int_data & 0x0000ff00) >> 8; --second byte + //(int_data & 0x00ff0000) >> 16; --third byte + //(int_data & 0xff000000) >> 24; --fourth byte + // Just a first byte from OPJ integer is needed for 8-bit depth images + } + // Increment write position indicator for each pixel's component we have written to the buffer + buffer_write_pos += channels; + } + } + +#ifdef DEBUG + logfile << "INFO :: OpenJPEG :: process() :: Copying image data took " << timer.getTime() << " microseconds" << endl + << flush; +#endif +} diff --git a/src/OpenJPEGImage.h b/src/OpenJPEGImage.h new file mode 100644 index 00000000..6ebcd51b --- /dev/null +++ b/src/OpenJPEGImage.h @@ -0,0 +1,165 @@ +/* IIP Server: OpenJPEG JPEG2000 handler + + Copyright (C) 2015 Moravian Library in Brno (http://www.mzk.cz/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _OPENJPEGIMAGE_H +#define _OPENJPEGIMAGE_H + +#include "IIPImage.h" + +#define TILESIZE 256 + +extern std::ofstream logfile; + +// Image class for JPEG 2000 Images: +// Inherits from IIPImage. Uses the OpenJPEG library. +class OpenJPEGImage : public IIPImage { + +private: + std::string filename; // Compressed source file + + unsigned int raster_width; // Image size + unsigned int raster_height; + + unsigned int image_tile_width; // Tile size defined in the image + unsigned int image_tile_height; + + int sgnd; // Whether the data are signed + + unsigned int max_layers; // Quality layers + + unsigned int virtual_levels; // How many virtual levels we need to generate + + /** + Main processing function + \param tw width of region + \param th height of region + \param xoffset x coordinate + \param yoffset y coordinate + \param res resolution + \param layers number of quality levels to decode + \param tile specific tile to decode (-1 if deconding a region) + \param d buffer to fill +*/ + void process(unsigned int tw, unsigned int th, + unsigned int xoffset, unsigned int yoffset, unsigned int res, + int layers, int tile, void* d) throw(file_error); + +public: + /** + Constructor + */ + OpenJPEGImage() + : IIPImage() + { + image_tile_width = 0; + image_tile_height = 0; + tile_width = TILESIZE; + tile_height = TILESIZE; + raster_width = 0; + raster_height = 0; + sgnd = 0; + numResolutions = 0; + virtual_levels = 0; + }; + + /** + Constructor + \param path image path + */ + OpenJPEGImage(const std::string& path) + : IIPImage(path) + { + image_tile_width = 0; + image_tile_height = 0; + tile_width = TILESIZE; + tile_height = TILESIZE; + raster_width = 0; + raster_height = 0; + sgnd = 0; + numResolutions = 0; + virtual_levels = 0; + }; + + /** + Copy Constructor + \param image IIPImage object + */ + OpenJPEGImage(const IIPImage& image) + : IIPImage(image) + { + tile_width = TILESIZE; + tile_height = TILESIZE; + numResolutions = image.numResolutions; + virtual_levels = 0; + }; + + /** + Destructor + */ + ~OpenJPEGImage() + { + closeImage(); + }; + + /** + Overloaded function for opening a JP2 image + */ + void openImage() throw(file_error); + + /** + Overloaded function for loading JP2 image information + \param x horizontal sequence angle + \param y vertical sequence angle + */ + void loadImageInfo(int x, int y) throw(file_error); + + /** + Overloaded function for closing a JP2 image + */ + void closeImage(); + + /** + Overloaded function for getting a particular tile + \param x horizontal sequence angle + \param y vertical sequence angle + \param r resolution + \param l number of quality layers to decode + \param t tile number + */ + RawTile getTile(int x, int y, unsigned int r, int l, + unsigned int t) throw(file_error); + + /** + Overloaded function for returning a region from image + \param ha horizontal angle + \param va vertical angle + \param r resolution + \param l number of quality layers to decode + \param x x coordinate + \param y y coordinate + \param w width of region + \param h height of region + \param b buffer to fill + \return a RawTile object + */ + void getRegion(int ha, int va, unsigned int r, int l, + int x, int y, unsigned int w, unsigned int h, + unsigned char* b) throw(file_error); +}; + +#endif diff --git a/src/Task.h b/src/Task.h index 339b303f..80277e1f 100644 --- a/src/Task.h +++ b/src/Task.h @@ -72,6 +72,7 @@ struct Session { IIPResponse* response; Watermark* watermark; int loglevel; + bool useOpenJPEG; std::ofstream* logfile; std::map headers;