diff --git a/src/cmake/add_oiio_plugin.cmake b/src/cmake/add_oiio_plugin.cmake index f66bf756e7..d722b4abb2 100644 --- a/src/cmake/add_oiio_plugin.cmake +++ b/src/cmake/add_oiio_plugin.cmake @@ -11,6 +11,7 @@ # [ NAME targetname ... ] # [ SRC source1 ... ] # [ INCLUDE_DIRS include_dir1 ... ] +# [ LINK_DIRECTORIES link_dir1 ... ] # [ LINK_LIBRARIES external_lib1 ... ] # [ COMPILE_OPTIONS -Wflag ... ] # [ DEFINITIONS FOO=bar ... ]) @@ -18,9 +19,9 @@ # The plugin name can be specified with NAME, otherwise is inferred from the # subdirectory name. The source files of the binary can be specified with # SRC, otherwise are inferred to be all the .cpp files within the -# subdirectory. Optional compile DEFINITIONS, private INCLUDE_DIRS, and -# private LINK_LIBRARIES may also be specified. The source is automatically -# linked against OpenImageIO. +# subdirectory. Optional compile DEFINITIONS, private INCLUDE_DIRS, private +# LINK_DIRECTORIES, and private LINK_LIBRARIES may also be specified. +# The source is automatically linked against OpenImageIO. # # The plugin may be disabled individually using any of the usual # check_is_enabled() conventions (e.g. -DENABLE_=OFF). @@ -35,7 +36,7 @@ # be handed off too the setup of the later OpenImageIO target. # macro (add_oiio_plugin) - cmake_parse_arguments (_plugin "" "NAME" "SRC;INCLUDE_DIRS;LINK_LIBRARIES;COMPILE_OPTIONS;DEFINITIONS" ${ARGN}) + cmake_parse_arguments (_plugin "" "NAME" "SRC;INCLUDE_DIRS;LINK_DIRECTORIES;LINK_LIBRARIES;COMPILE_OPTIONS;DEFINITIONS" ${ARGN}) # Arguments: args... get_filename_component (_plugin_name ${CMAKE_CURRENT_SOURCE_DIR} NAME_WE) if (NOT _plugin_NAME) @@ -64,6 +65,7 @@ macro (add_oiio_plugin) set (format_plugin_definitions ${format_plugin_definitions} ${_plugin_DEFINITIONS} PARENT_SCOPE) set (format_plugin_compile_options ${format_plugin_compile_options} ${_plugin_COMPILE_OPTIONS} PARENT_SCOPE) set (format_plugin_include_dirs ${format_plugin_include_dirs} ${_plugin_INCLUDE_DIRS} PARENT_SCOPE) + set (format_plugin_lib_dirs ${format_plugin_lib_dirs} ${_plugin_LINK_DIRECTORIES} PARENT_SCOPE) set (format_plugin_libs ${format_plugin_libs} ${_plugin_LINK_LIBRARIES} PARENT_SCOPE) else () # # Get the name of the current directory and use it as the target name. @@ -74,6 +76,8 @@ macro (add_oiio_plugin) OpenImageIO_EXPORTS) target_compile_options (${_plugin_NAME} PRIVATE ${_plugin_COMPILE_OPTIONS}) target_include_directories (${_plugin_NAME} BEFORE PRIVATE ${_plugin_INCLUDE_DIRS}) + target_link_directories (${_plugin_NAME} PUBLIC OpenImageIO + PRIVATE ${_plugin_LINK_DIRECTORIES}) target_link_libraries (${_plugin_NAME} PUBLIC OpenImageIO PRIVATE ${_plugin_LINK_LIBRARIES}) set_target_properties (${_plugin_NAME} PROPERTIES PREFIX "" FOLDER "Plugins") diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake index eee6373d84..2c4a3da782 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -174,6 +174,8 @@ checked_find_package (OpenJPEG VERSION_MIN 2.0 # Note: Recent OpenJPEG versions have exported cmake configs, but we don't # find them reliable at all, so we stick to our FindOpenJPEG.cmake module. +checked_find_package (OpenJPH VERSION_MIN 0.21) + checked_find_package (OpenVDB VERSION_MIN 9.0 DEPS TBB diff --git a/src/cmake/modules/FindOpenJPEG.cmake b/src/cmake/modules/FindOpenJPEG.cmake index b3802d5d2c..907f6a8b84 100644 --- a/src/cmake/modules/FindOpenJPEG.cmake +++ b/src/cmake/modules/FindOpenJPEG.cmake @@ -17,6 +17,46 @@ include (FindPackageHandleStandardArgs) include (FindPackageMessage) include (SelectLibraryConfigurations) + + +if(DEFINED OPENJPEG_ROOT) + set(_openjpeg_pkgconfig_path "${OPENJPEG_ROOT}/lib/pkgconfig") + if(EXISTS "${_openjpeg_pkgconfig_path}") + set(ENV{PKG_CONFIG_PATH} "${_openjpeg_pkgconfig_path}:$ENV{PKG_CONFIG_PATH}") + endif() +endif() + + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(OPENJPEG_PC QUIET openjpeg) +endif() + +if(OPENJPEG_PC_FOUND) + set(OPENJPEG_FOUND TRUE) + set(OPENJPEG_VERSION ${OPENJPEG_PC_VERSION}) + set(OPENJPEG_INCLUDES ${OPENJPEG_PC_INCLUDE_DIRS}) + set(OPENJPEG_LIBRARIES ${OPENJPEG_PC_LIBRARIES}) + if(NOT OPENJPEG_FIND_QUIETLY) + FIND_PACKAGE_MESSAGE(OPENJPEG + "Found OPENJPEG via pkg-config: v${OPENJPEG_VERSION} ${OPENJPEG_LIBRARIES}" + "[${OPENJPEG_INCLUDES}][${OPENJPEG_LIBRARIES}]" + ) + endif() +else() + set(OPENJPEG_FOUND FALSE) + set(OPENJPEG_VERSION 0.0.0) + set(OPENJPEG_INCLUDES "") + set(OPENJPEG_LIBRARIES "") + if(NOT OPENJPEG_FIND_QUIETLY) + FIND_PACKAGE_MESSAGE(OPENJPEG + "Could not find OPENJPEG via pkg-config" + "[${OPENJPEG_INCLUDES}][${OPENJPEG_LIBRARIES}]" + ) + endif() +endif() + + macro (PREFIX_FIND_INCLUDE_DIR prefix includefile libpath_var) string (TOUPPER ${prefix}_INCLUDE_DIR tmp_varname) find_path(${tmp_varname} ${includefile} diff --git a/src/cmake/modules/FindOpenJPH.cmake b/src/cmake/modules/FindOpenJPH.cmake new file mode 100644 index 0000000000..4e6ea4a17b --- /dev/null +++ b/src/cmake/modules/FindOpenJPH.cmake @@ -0,0 +1,57 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +# Module to find OPENJPH. +# +# This module will first look into the directories defined by the variables: +# - OPENJPH_ROOT +# +# This module defines the following variables: +# +# OPENJPH_INCLUDES - where to find ojph_arg.h +# OPENJPH_LIBRARIES - list of libraries to link against when using OPENJPH. +# OPENJPH_FOUND - True if OPENJPH was found. +# OPENJPH_VERSION - Set to the OPENJPH version found +include (FindPackageHandleStandardArgs) +include (FindPackageMessage) +include (SelectLibraryConfigurations) + +if(DEFINED OPENJPH_ROOT) + set(_openjph_pkgconfig_path "${OPENJPH_ROOT}/lib/pkgconfig") + if(EXISTS "${_openjph_pkgconfig_path}") + set(ENV{PKG_CONFIG_PATH} "${_openjph_pkgconfig_path}:$ENV{PKG_CONFIG_PATH}") + endif() +endif() + + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(OPENJPH_PC QUIET openjph) +endif() + +if(OPENJPH_PC_FOUND) + set(OPENJPH_FOUND TRUE) + set(OPENJPH_VERSION ${OPENJPH_PC_VERSION}) + set(OPENJPH_INCLUDES ${OPENJPH_PC_INCLUDE_DIRS}) + set(OPENJPH_LIBRARY_DIRS ${OPENJPH_PC_LIBDIR}) + set(OPENJPH_LIBRARIES ${OPENJPH_PC_LIBRARIES}) + + if(NOT OPENJPH_FIND_QUIETLY) + FIND_PACKAGE_MESSAGE(OPENJPH + "Found OPENJPH via pkg-config: v${OPENJPH_VERSION} ${OPENJPH_LIBRARIES}" + "[${OPENJPH_INCLUDES}][${OPENJPH_LIBRARIES}]" + ) + endif() +else() + set(OPENJPH_FOUND FALSE) + set(OPENJPH_VERSION 0.0.0) + set(OPENJPH_INCLUDES "") + set(OPENJPH_LIBRARIES "") + if(NOT OPENJPH_FIND_QUIETLY) + FIND_PACKAGE_MESSAGE(OPENJPH + "Could not find OPENJPH via pkg-config" + "[${OPENJPH_INCLUDES}][${OPENJPH_LIBRARIES}]" + ) + endif() +endif() \ No newline at end of file diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index e3e6a20b3e..0bbc77c716 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -274,6 +274,9 @@ macro (oiio_add_all_tests) oiio_add_tests (jpeg2000 FOUNDVAR OPENJPEG_FOUND IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images") + oiio_add_tests (htj2k + FOUNDVAR OPENJPH_FOUND + IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images") oiio_add_tests (jpeg2000-j2kp4files FOUNDVAR OPENJPEG_FOUND IMAGEDIR j2kp4files_v1_5 diff --git a/src/doc/builtinplugins.rst b/src/doc/builtinplugins.rst index dfe1b4b00e..ac76afbe01 100644 --- a/src/doc/builtinplugins.rst +++ b/src/doc/builtinplugins.rst @@ -1143,6 +1143,11 @@ JPEG-2000 is not yet widely used, so OpenImageIO's support of it is preliminary. In particular, we are not yet very good at handling the metadata robustly. +Optionally this plugin can be built with OpenJPH support, which is a +JPEG-2000 encoder/decoder that is faster than OpenJPEG, and supports the +High Throughput JPEG2000 (HTJ2K) format (Jpeg2000 Part 15). If OpenJPH is not available, the +OpenJPEG library will be used instead but only for decoding. OpenJPH is available at https://github.com/aous72/OpenJPH . + **Attributes** .. list-table:: @@ -1186,6 +1191,9 @@ attributes are supported: - ptr - Pointer to a ``Filesystem::IOProxy`` that will handle the I/O, for example by reading from memory rather than the file system. + +If OpenJPH is installed, the reader will attempt to read the file first with +the OpenJPH library, and if that fails, it will fall back to the OpenJPEG library. **Configuration settings for JPEG-2000 output** @@ -1215,14 +1223,52 @@ control aspects of the writing itself: for output rather than being assumed to be associated and get automatic un-association to store in the file. +If OpenJPH is installed, and the file extension is :file:`.j2c`, or if the -``compression`` flag is set to ``"htj2k"``, the +writer will attempt to write the file with the OpenJPH library, and the following flags will be available: + +.. list-table:: + :widths: 30 10 65 + :header-rows: 1 + + * - Output Configuration Attribute + - Type + - Meaning + * - ``jph:bit_depth`` + - int + - The output bitdepth of the file. + * - ``jph:num_decomps`` + - int + - (5) number of decompositions. + * - ``jph:block_size`` + - string + - The output block size, defaults to 64,64 + * - ``jph:prog_order`` + - string + - (RPCL) is the progression order, and can be one of: + LRCP, RLCP, RPCL, PCRL, CPRL. These determine the sequence in which the image data is processed and transmitted. The letters stand for: + R: Resolution + P: position + C: component + L: Layer + RPCL is common for applications where resolution scalability is important. + * - ``jph:precincts`` + - string + - x,y,x,y,...,x,y where x,y is the precinct size + starting from the coarsest resolution; the last precinct + is repeated for all finer resolutions + * - ``jph:qstep`` + - float + - If supplied, is the quantization step size for lossy compression; + quantization steps size for all subbands are derived from this value. Valid values can be from 0.00001 to 0.5. + If not used, the encoder will be lossless. + + **Custom I/O Overrides** JPEG-2000 input and output both support the "custom I/O" feature via the special ``"oiio:ioproxy"`` attributes (see Sections :ref:`sec-imageoutput-ioproxy` and :ref:`sec-imageinput-ioproxy`) as well as the `set_ioproxy()` methods. - - | .. _sec-bundledplugins-jpegxl: diff --git a/src/jpeg2000.imageio/CMakeLists.txt b/src/jpeg2000.imageio/CMakeLists.txt index 2bce609681..910ae5811d 100644 --- a/src/jpeg2000.imageio/CMakeLists.txt +++ b/src/jpeg2000.imageio/CMakeLists.txt @@ -3,10 +3,24 @@ # https://github.com/AcademySoftwareFoundation/OpenImageIO if (OPENJPEG_FOUND) - add_oiio_plugin (jpeg2000input.cpp jpeg2000output.cpp - INCLUDE_DIRS ${OPENJPEG_INCLUDES} - LINK_LIBRARIES ${OPENJPEG_LIBRARIES} - DEFINITIONS "USE_OPENJPEG") + set(_jpeg2000_includes ${OPENJPEG_INCLUDES}) + set(_jpeg2000_lib_dirs ${OPENJPEG_LIBRARY_DIRS}) + set(_jpeg2000_libs ${OPENJPEG_LIBRARIES}) + set(_jpeg2000_defs "USE_OPENJPEG") + + if (OPENJPH_FOUND) + list(APPEND _jpeg2000_includes ${OPENJPH_INCLUDES}) + list(APPEND _jpeg2000_lib_dirs ${OPENJPH_LIBRARY_DIRS}) + list(APPEND _jpeg2000_libs ${OPENJPH_LIBRARIES}) + list(APPEND _jpeg2000_defs "USE_OPENJPH") + endif() + + add_oiio_plugin(jpeg2000input.cpp jpeg2000output.cpp + INCLUDE_DIRS ${_jpeg2000_includes} + LINK_DIRECTORIES ${_jpeg2000_lib_dirs} + LINK_LIBRARIES ${_jpeg2000_libs} + DEFINITIONS ${_jpeg2000_defs} + ) else() message (WARNING "Jpeg-2000 plugin will not be built") endif() diff --git a/src/jpeg2000.imageio/jpeg2000input.cpp b/src/jpeg2000.imageio/jpeg2000input.cpp index 7af24e6021..fb974f003c 100644 --- a/src/jpeg2000.imageio/jpeg2000input.cpp +++ b/src/jpeg2000.imageio/jpeg2000input.cpp @@ -14,6 +14,14 @@ #include #include +#ifdef USE_OPENJPH +# include +# include +# include +# include +# include +#endif + #ifndef OIIO_OPJ_VERSION # if defined(OPJ_VERSION_MAJOR) // OpenJPEG >= 2.1 defines these symbols @@ -70,6 +78,7 @@ j2k_associateAlpha(T* data, int size, int channels, int alpha_channel, } // namespace + class Jpeg2000Input final : public ImageInput { public: Jpeg2000Input() { init(); } @@ -177,6 +186,18 @@ class Jpeg2000Input final : public ImageInput { } static void openjpeg_dummy_callback(const char* /*msg*/, void* /*data*/) {} + +#ifdef USE_OPENJPH + +private: // OJPH code + bool ojph_read_header(); // Read the header and set up the spec + bool ojph_read_image(); // We need to read the image once. + bool ojph_image_read = false; // Have we read the image yet? + bool ojph_reader = false; // Are we using the ojph reader? + std::vector m_buf; // Buffer the image pixels + int buffer_bpp; // Bytes per pixel in the buffer + ojph::codestream codestream; // The HTJ2K codestream +#endif }; @@ -194,7 +215,10 @@ jpeg2000_input_imageio_create() { return new Jpeg2000Input; } -OIIO_EXPORT const char* jpeg2000_input_extensions[] = { "jp2", "j2k", "j2c", +OIIO_EXPORT const char* jpeg2000_input_extensions[] = { "jp2", "j2k", +#ifdef USE_OPENJPH + "j2c", +#endif nullptr }; OIIO_PLUGIN_EXPORTS_END @@ -210,6 +234,8 @@ Jpeg2000Input::init(void) ioproxy_clear(); } + + bool Jpeg2000Input::valid_file(Filesystem::IOProxy* ioproxy) const { @@ -224,6 +250,223 @@ Jpeg2000Input::valid_file(Filesystem::IOProxy* ioproxy) const return is_jp2_header(header) || is_j2k_header(header); } +#ifdef USE_OPENJPH +// A wrapper for ojph::infile_base to use OIIO's IOProxy +class jph_infile : public ojph::infile_base { +private: + Filesystem::IOProxy* ioproxy; + +public: + jph_infile(Filesystem::IOProxy* iop) { ioproxy = iop; } + ~jph_infile() + { + // if (ioproxy != NULL) + // ioproxy->close(); + } + + //read reads size bytes, returns the number of bytes read + size_t read(void* ptr, size_t size) { return ioproxy->read(ptr, size); } + //seek returns 0 on success + int seek(ojph::si64 offset, enum infile_base::seek origin) + { + return ioproxy->seek(offset, origin); + } + ojph::si64 tell() { return ioproxy->tell(); }; + bool eof() { return ioproxy->tell() == ioproxy->size(); } + void close() + { + ioproxy->close(); + ioproxy = NULL; + }; +}; + + + +// Convert a 32-bit signed integer to a 16-bit signed integer, with special +// handling for special numbers (NaN, Infinity, etc.) if requested. +ojph::si16 +convert_si32_to_si16(const ojph::si32 si32_value, + bool convert_special_numbers_to_finite_numbers = false) +{ + if (si32_value > INT16_MAX) + return INT16_MAX; + else if (si32_value < INT16_MIN) + return INT16_MIN; + else if (true == convert_special_numbers_to_finite_numbers) { + const ojph::si16 si16_value = (ojph::si16)si32_value; + half half_value; + half_value.setBits(si16_value); + if (half_value.isFinite()) + return si16_value; + + // handle non-real number to real-number mapping + if (half_value.isNan()) + half_value = 0.0f; + else if (half_value.isInfinity() && !half_value.isNegative()) + half_value = HALF_MAX; + else if (half_value.isInfinity() && half_value.isNegative()) + half_value = -1.0f * HALF_MAX; + + return half_value.bits(); + } else + return (ojph::si16)si32_value; +} + + + +bool +Jpeg2000Input::ojph_read_header() +{ + ojph::param_siz siz = codestream.access_siz(); + int ch = siz.get_num_components(); + const int w = siz.get_recon_width(0); + const int h = siz.get_recon_height(0); + TypeDesc dtype; + + if (ch > 4) + ch = 4; // Only do the first 4 channels. + m_bpp.resize(ch); + + for (int c = 0; c < ch; c++) { + switch (siz.get_bit_depth(c)) { + case 8: + dtype = TypeDesc::UCHAR; + m_bpp[c] = 1; + break; + case 10: + case 12: + case 16: + m_bpp[c] = 2; + dtype = TypeDesc::USHORT; + break; + case 32: + m_bpp[c] = 4; + dtype = TypeDesc::UINT; + break; + default: + errorfmt("Unsupported bit depth {} for channel {}", + siz.get_bit_depth(c), c); + close(); + return false; + } + if (m_bpp[c] != m_bpp[0]) { + errorfmt("All channels need to be the same bitdepth"); + close(); + return false; + } + } + + m_spec = ImageSpec(w, h, ch, dtype); + m_spec.default_channel_names(); + m_spec.attribute("oiio:BitsPerSample", siz.get_bit_depth(0)); + m_spec.set_colorspace("sRGB"); + + return true; +} + + + +bool +Jpeg2000Input::ojph_read_image() +{ + buffer_bpp = m_bpp[0]; + int w = m_spec.width; + int h = m_spec.height; + int ch = m_spec.nchannels; + ojph::param_siz siz = codestream.access_siz(); + + const int bufsize = w * h * ch * buffer_bpp; + m_buf.resize(bufsize); + codestream.create(); + + int file_bit_depth = siz.get_bit_depth(0); // Assuming RGBA are the same. + + // We are going to read the whole image into the buffer, since with openjph + // its hard to easily grab part of the image. + if (codestream.is_planar()) { + for (ojph::ui32 c = 0; c < ch; ++c) + + for (ojph::ui32 i = 0; i < h; ++i) { + ojph::ui32 comp_num; + ojph::line_buf* line = codestream.pull(comp_num); + const ojph::si32* sp = line->i32; + assert(comp_num == c); + if (m_spec.format == TypeDesc::UCHAR) { + unsigned char* dout = &m_buf[i * w * ch]; + dout += c; + for (ojph::ui32 j = w; j > 0; j--, dout += ch) { + *dout = *sp++; + } + } + if (m_spec.format == TypeDesc::USHORT) { + unsigned short* dout + = (unsigned short*)&m_buf[buffer_bpp * (i * w * ch)]; + dout += c; + for (ojph::ui32 j = w; j > 0; j--, dout += ch) { + *dout = bit_range_convert(*sp++, file_bit_depth, + buffer_bpp * 8); + } + } + } + + } else { + for (ojph::ui32 i = 0; i < h; ++i) { + for (ojph::ui32 c = 0; c < ch; ++c) { + ojph::ui32 comp_num; + ojph::line_buf* line = codestream.pull(comp_num); + const ojph::si32* sp = line->i32; + assert(comp_num == c); + if (m_spec.format == TypeDesc::UCHAR) { + unsigned char* dout = &m_buf[i * w * ch]; + dout += c; + for (ojph::ui32 j = w; j > 0; j--, dout += ch) { + *dout = *sp++; + } + } + if (m_spec.format == TypeDesc::USHORT) { + unsigned short* dout + = (unsigned short*)&m_buf[buffer_bpp * (i * w * ch)]; + dout += c; + for (ojph::ui32 j = w; j > 0; j--, dout += ch) { + *dout = bit_range_convert(*sp++, file_bit_depth, + buffer_bpp * 8); + } + } + } + } + } + + ojph_image_read = true; + return true; +} + + + +class Oiio_Reader_Error_handler : public ojph::message_error { + // This is a special error handler, since in this case, if we get the error-code for not a J2K file, we dont + // want to print out anything. If not, we fall through to the regular error handler. + ojph::message_error* default_error; + +public: + Oiio_Reader_Error_handler(ojph::message_error* error) + { + default_error = error; + } + virtual void operator()(int error_code, const char* file_name, int line_num, + const char* fmt, ...) + { + if (error_code == 0x00050044) { + throw std::runtime_error("ojph error: not HTJ2K file"); + } + va_list args; + va_start(args, fmt); + default_error[0](error_code, file_name, line_num, fmt, args); + va_end(args); + } +}; + +#endif // USE_OPENJPH + bool Jpeg2000Input::open(const std::string& name, ImageSpec& p_spec) { @@ -231,6 +474,25 @@ Jpeg2000Input::open(const std::string& name, ImageSpec& p_spec) if (!ioproxy_use_or_open(name)) return false; + +#ifdef USE_OPENJPH + jph_infile* jphinfile = new jph_infile(ioproxy()); + ojph_reader = true; + ojph::message_error* default_error = ojph::get_error(); + + try { + Oiio_Reader_Error_handler error_handler(default_error); + ojph::configure_error(&error_handler); + codestream.read_headers(jphinfile); + return ojph_read_header(); + } catch (const std::runtime_error& e) { + ojph::configure_error(default_error); + ojph_reader = false; + } + delete jphinfile; + +#endif // USE_OPENJPH + ioseek(0); m_codec = create_decompressor(); @@ -380,6 +642,7 @@ Jpeg2000Input::open(const std::string& name, ImageSpec& newspec, } + bool Jpeg2000Input::read_native_scanline(int subimage, int miplevel, int y, int z, void* data) @@ -388,10 +651,23 @@ Jpeg2000Input::read_native_scanline(int subimage, int miplevel, int y, int z, if (!seek_subimage(subimage, miplevel)) return false; - if (m_spec.format == TypeDesc::UINT8) - read_scanline(y, z, data); - else - read_scanline(y, z, data); +#ifdef USE_OPENJPH + if (ojph_reader) { + if (!ojph_image_read) + ojph_read_image(); + unsigned char* start + = &m_buf[buffer_bpp * (y * m_spec.width * m_spec.nchannels)]; + memcpy(data, start, buffer_bpp * m_spec.width * m_spec.nchannels); + } else { +#endif // USE_OPENJPH + + if (m_spec.format == TypeDesc::UINT8) + read_scanline(y, z, data); + else + read_scanline(y, z, data); +#ifdef USE_OPENJPH + } +#endif // JPEG2000 specifically dictates unassociated (un-"premultiplied") alpha. // Convert to associated unless we were requested not to do so. @@ -405,6 +681,7 @@ Jpeg2000Input::read_native_scanline(int subimage, int miplevel, int y, int z, m_spec.nchannels, m_spec.alpha_channel, gamma); } + return true; } @@ -423,6 +700,8 @@ Jpeg2000Input::close(void) return true; } + + bool Jpeg2000Input::is_jp2_header(const uint8_t header[12]) { @@ -431,6 +710,8 @@ Jpeg2000Input::is_jp2_header(const uint8_t header[12]) return memcmp(header, jp2_header, sizeof(jp2_header)) == 0; } + + bool Jpeg2000Input::is_j2k_header(const uint8_t header[5]) { @@ -438,6 +719,8 @@ Jpeg2000Input::is_j2k_header(const uint8_t header[5]) return memcmp(header, j2k_header, sizeof(j2k_header)) == 0; } + + opj_codec_t* Jpeg2000Input::create_decompressor() { diff --git a/src/jpeg2000.imageio/jpeg2000output.cpp b/src/jpeg2000.imageio/jpeg2000output.cpp index 1846593519..f5af21f473 100644 --- a/src/jpeg2000.imageio/jpeg2000output.cpp +++ b/src/jpeg2000.imageio/jpeg2000output.cpp @@ -23,6 +23,13 @@ # endif #endif +#ifdef USE_OPENJPH +# include +# include +# include +# include +# include +#endif OIIO_PLUGIN_NAMESPACE_BEGIN @@ -57,12 +64,21 @@ class Jpeg2000Output final : public ImageOutput { std::vector m_tilebuffer; std::vector m_scratch; + +#ifdef USE_OPENJPH + // opj_cparameters_t m_compression_parameters; + std::unique_ptr m_jph_image; + std::unique_ptr m_jph_stream; + int output_depth; +#endif + void init(void) { m_image = NULL; m_codec = NULL; m_stream = NULL; m_convert_alpha = true; + ioproxy_clear(); } @@ -86,6 +102,10 @@ class Jpeg2000Output final : public ImageOutput { opj_stream_destroy(m_stream); m_stream = NULL; } +#ifdef USE_OPENJPH + m_jph_stream.reset(); + m_jph_image.reset(); +#endif } bool save_image(); @@ -129,6 +149,12 @@ class Jpeg2000Output final : public ImageOutput { } static void openjpeg_dummy_callback(const char* /*msg*/, void* /*data*/) {} + +#ifdef USE_OPENJPH + void create_jph_image(); + template + void write_jph_scanline(int y, int /*z*/, const void* data); +#endif }; @@ -142,11 +168,16 @@ jpeg2000_output_imageio_create() } OIIO_EXPORT const char* jpeg2000_output_extensions[] = { "jp2", "j2k", +#ifdef USE_OPENJPH + "j2c", "jph", +#endif nullptr }; + OIIO_PLUGIN_EXPORTS_END + bool Jpeg2000Output::open(const std::string& name, const ImageSpec& spec, OpenMode mode) @@ -176,6 +207,42 @@ Jpeg2000Output::open(const std::string& name, const ImageSpec& spec, if (m_spec.tile_width && m_spec.tile_height) m_tilebuffer.resize(m_spec.image_bytes()); + const ParamValue* compressionparams = m_spec.find_attribute("compression", + TypeString); +#ifdef USE_OPENJPH + + bool use_openjph = false; + + // If a j2c file is specified, we default to j2c + // otherwise we check the compression parameter. + + std::string compressionparms_str; + if (compressionparams) { + compressionparms_str = compressionparams->get_string(); + if (compressionparms_str.compare(0, 5, "htj2k") == 0) + use_openjph = true; + } + std::string ext = Filesystem::extension(name); + if (ext == ".j2c") + // TODO - Need to check if j2c files can be created with openjpeg + use_openjph = true; + + if (use_openjph) { + create_jph_image(); + return true; + } +#else + // If we are not using OpenJPH, we need to create a JPEG2000 image. + // This is the default behavior. + std::string compressionparms_str; + if (compressionparams) { + compressionparms_str = compressionparams->get_string(); + if (compressionparms_str.compare(0, 5, "htj2k") == 0) { + errorfmt("OpenJPH not enabled, cannot create HTJ2K file"); + return false; + } + } +#endif m_image = create_jpeg2000_image(); return true; } @@ -242,10 +309,18 @@ Jpeg2000Output::write_scanline(int y, int z, TypeDesc format, const void* data, m_spec.nchannels, m_spec.alpha_channel, 2.2f); } - if (m_spec.format == TypeDesc::UINT8) - write_scanline(y, z, data); - else - write_scanline(y, z, data); +#ifdef USE_OPENJPH + if (m_jph_image) { + if (m_spec.format == TypeDesc::UINT8) + write_jph_scanline(y, z, data); + else + write_jph_scanline(y, z, data); + } else +#endif // USE_OPENJPH + if (m_spec.format == TypeDesc::UINT8) + write_scanline(y, z, data); + else + write_scanline(y, z, data); if (y == m_spec.height - 1) save_image(); @@ -283,6 +358,15 @@ Jpeg2000Output::close() std::vector().swap(m_tilebuffer); } +#ifdef USE_OPENJPH + if (m_jph_image) { + m_jph_stream->flush(); + m_jph_stream->close(); + destroy_stream(); + return true; + } +#endif + if (m_image) { opj_image_destroy(m_image); m_image = NULL; @@ -298,6 +382,15 @@ Jpeg2000Output::close() bool Jpeg2000Output::save_image() { +#ifdef USE_OPENJPH + if (m_jph_stream) { + m_jph_stream->flush(); + m_jph_stream->close(); + destroy_stream(); + return true; + } +#endif + m_codec = create_compressor(); if (!m_codec) return false; @@ -340,6 +433,7 @@ Jpeg2000Output::save_image() } + opj_image_t* Jpeg2000Output::create_jpeg2000_image() { @@ -395,6 +489,7 @@ Jpeg2000Output::create_jpeg2000_image() } + inline void Jpeg2000Output::init_components(opj_image_cmptparm_t* components, int precision) { @@ -414,6 +509,7 @@ Jpeg2000Output::init_components(opj_image_cmptparm_t* components, int precision) } + opj_codec_t* Jpeg2000Output::create_compressor() { @@ -502,6 +598,7 @@ Jpeg2000Output::setup_cinema_compression(OPJ_RSIZ_CAPABILITIES p_rsizCap) } + void Jpeg2000Output::setup_compression_params() { @@ -547,6 +644,8 @@ Jpeg2000Output::setup_compression_params() m_compression_parameters.mode = *(int*)compression_mode->data(); } + + OPJ_PROG_ORDER Jpeg2000Output::get_progression_order(const std::string& progression_order) { @@ -563,4 +662,183 @@ Jpeg2000Output::get_progression_order(const std::string& progression_order) return OPJ_PROG_UNKNOWN; } + +#ifdef USE_OPENJPH + + +struct size_list_interpreter : public ojph::cli_interpreter::arg_inter_base { + size_list_interpreter(const int max_num_elements, int& num_elements, + ojph::size* list) + : max_num_eles(max_num_elements) + , sizelist(list) + , num_eles(num_elements) + { + } + + virtual void operate(const char* str) + { + const char* next_char = str; + num_eles = 0; + do { + if (num_eles) { + if (*next_char != ',') //separate sizes by a comma + throw "sizes in a sizes list must be separated by a comma"; + next_char++; + } + + char* endptr; + sizelist[num_eles].w = (ojph::ui32)strtoul(next_char, &endptr, 10); + if (endptr == next_char) + throw "size number is improperly formatted"; + next_char = endptr; + if (*next_char != ',') + throw "size must have a " + "," + " between the two numbers"; + next_char++; + sizelist[num_eles].h = (ojph::ui32)strtoul(next_char, &endptr, 10); + if (endptr == next_char) + throw "number is improperly formatted"; + next_char = endptr; + + + ++num_eles; + } while (*next_char == ',' && num_eles < max_num_eles); + if (num_eles < max_num_eles) { + if (*next_char) + throw "size elements must separated by a " + "," + ""; + } else if (*next_char) + throw "there are too many elements in the size list"; + } + + const int max_num_eles; + ojph::size* sizelist; + int& num_eles; +}; + + + +void +Jpeg2000Output::create_jph_image() +{ + m_jph_stream = std::make_unique(); + ojph::param_siz siz = m_jph_stream->access_siz(); + siz.set_image_extent(ojph::point(m_spec.width, m_spec.height)); + + + // TODO + /* + OPJ_COLOR_SPACE color_space = OPJ_CLRSPC_SRGB; + if (m_spec.nchannels == 1) + color_space = OPJ_CLRSPC_GRAY; + */ + + int precision = 16; + const ParamValue* prec = m_spec.find_attribute("oiio:BitsPerSample", + TypeDesc::INT); + bool is_signed = false; + + if (prec) + precision = *(int*)prec->data(); + + switch (m_spec.format.basetype) { + case TypeDesc::INT8: + case TypeDesc::UINT8: + precision = 8; + is_signed = false; + break; + case TypeDesc::FLOAT: + case TypeDesc::HALF: + case TypeDesc::DOUBLE: + throw "OpenJPH::Write Double is not currently supported."; + default: break; + } + + output_depth = m_spec.get_int_attribute("jph:bit_depth", precision); + + siz.set_num_components(m_spec.nchannels); + ojph::point subsample(1, 1); // Default subsample + for (ojph::ui32 c = 0; c < m_spec.nchannels; ++c) + siz.set_component(c, subsample, output_depth, is_signed); + + ojph::size tile_size(0, 0); + ojph::point tile_offset(0, 0); + ojph::point image_offset(0, 0); + siz.set_image_offset(image_offset); + siz.set_tile_size(tile_size); + siz.set_tile_offset(tile_offset); + ojph::param_cod cod = m_jph_stream->access_cod(); + + std::string block_args = m_spec.get_string_attribute("jph:block_size", + "64,64"); + std::stringstream ss(block_args); + char comma; + int block_size_x, block_size_y; + ss >> block_size_x >> comma >> block_size_y; + + cod.set_block_dims(block_size_x, block_size_y); + cod.set_color_transform(true); + + int num_precincts = -1; + const int max_precinct_sizes = 33; //maximum number of decompositions is 32 + ojph::size precinct_size[max_precinct_sizes]; + std::string precinct_size_args + = m_spec.get_string_attribute("jph:precincts", "undef"); + if (precinct_size_args != "undef") { + size_list_interpreter sizelist(max_precinct_sizes, num_precincts, + precinct_size); + sizelist.operate(precinct_size_args.c_str()); + + if (num_precincts != -1) + cod.set_precinct_size(num_precincts, precinct_size); + } + + std::string progression_order + = m_spec.get_string_attribute("jph:prog_order", "RPCL"); + + cod.set_progression_order(progression_order.c_str()); + + cod.set_reversible(true); + + float qstep = m_spec.get_float_attribute("jph:qstep", -1); + + if (qstep > 0) { + cod.set_reversible(false); + m_jph_stream->access_qcd().set_irrev_quant(qstep); + } + + cod.set_num_decomposition(m_spec.get_int_attribute("jph:num_decomps", 5)); + m_jph_stream->set_planar(false); + m_jph_image = std::make_unique(); + m_jph_image->open(m_filename.c_str()); + m_jph_stream->write_headers(m_jph_image.get()); //, "test comment", 1); +} + + + +template +void +Jpeg2000Output::write_jph_scanline(int y, int /*z*/, const void* data) +{ + int bits = sizeof(T) * 8; + const T* scanline = static_cast(data); + ojph::ui32 next_comp = 0; + ojph::line_buf* cur_line = m_jph_stream->exchange(NULL, next_comp); + for (int c = 0; c < m_spec.nchannels; ++c) { + assert(c == next_comp); + for (int i = 0, j = c; i < m_spec.width; i++) { + unsigned int val = scanline[j]; + j += m_spec.nchannels; + if (bits != output_depth) + val = bit_range_convert(val, bits, output_depth); + cur_line->i32[i] = val; + } + cur_line = m_jph_stream->exchange(cur_line, next_comp); + } +} + +#endif + OIIO_PLUGIN_NAMESPACE_END diff --git a/src/libOpenImageIO/CMakeLists.txt b/src/libOpenImageIO/CMakeLists.txt index dcd3059b3e..aa98125e86 100644 --- a/src/libOpenImageIO/CMakeLists.txt +++ b/src/libOpenImageIO/CMakeLists.txt @@ -150,6 +150,8 @@ endif () target_compile_features (OpenImageIO INTERFACE cxx_std_${DOWNSTREAM_CXX_STANDARD}) +target_link_directories (OpenImageIO PRIVATE ${format_plugin_lib_dirs}) + target_link_libraries (OpenImageIO PUBLIC OpenImageIO_Util diff --git a/testsuite/htj2k/ref/out.txt b/testsuite/htj2k/ref/out.txt new file mode 100644 index 0000000000..84204b12c8 --- /dev/null +++ b/testsuite/htj2k/ref/out.txt @@ -0,0 +1,6 @@ +Comparing "../../../../../../../Volumes/git/OpenImageIO/build/testsuite/oiio-images/tahoe-gps.jpg" and "test.j2c" +PASS +Comparing "../../../../../../../Volumes/git/OpenImageIO/build/testsuite/oiio-images/dpx_nuke_10bits_rgb.dpx" and "testdpx.j2c" +PASS +Comparing "../../../../../../../Volumes/git/OpenImageIO/build/testsuite/oiio-images/tahoe-gps.jpg" and "testcompress.j2c" +PASS diff --git a/testsuite/htj2k/run.py b/testsuite/htj2k/run.py new file mode 100644 index 0000000000..006174e482 --- /dev/null +++ b/testsuite/htj2k/run.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + + +# These tests are checking the openjph library that can optionally be compiled into the Jpeg2000 +# plugin of OIIO. If the library is not enabled, these will fail. + +command += oiiotool(OIIO_TESTSUITE_IMAGEDIR+"/tahoe-gps.jpg" + " -o test.j2c") + +command += diff_command(OIIO_TESTSUITE_IMAGEDIR+"/tahoe-gps.jpg", "test.j2c") + +command += oiiotool(OIIO_TESTSUITE_IMAGEDIR+"/dpx_nuke_10bits_rgb.dpx" + " -o testdpx.j2c") + +command += diff_command(OIIO_TESTSUITE_IMAGEDIR+"/dpx_nuke_10bits_rgb.dpx", "testdpx.j2c") + + +command += oiiotool(OIIO_TESTSUITE_IMAGEDIR+"/tahoe-gps.jpg" + " --attrib qstep 0.03 -o testcompress.j2c") + +command += diff_command(OIIO_TESTSUITE_IMAGEDIR+"/tahoe-gps.jpg", "testcompress.j2c", extraargs="-fail 0.11") \ No newline at end of file