diff --git a/.gitignore b/.gitignore index d3cfb473ae..c8c897e967 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,6 @@ _coverage/ /*.exr /*.tif /*.jpg +/*.jxl /*.tx /*.log diff --git a/README.md b/README.md index 44fe174851..fdcaa2d03b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ OpenImageIO consists of: plugin can be found at runtime. * Plugins implementing I/O for several popular image file formats, - including TIFF, JPEG/JFIF, OpenEXR, PNG, HDR/RGBE, ICO, BMP, Targa, + including TIFF, JPEG/JFIF, JPEG XL, OpenEXR, PNG, HDR/RGBE, ICO, BMP, Targa, JPEG-2000, RMan Zfile, FITS, DDS, Softimage PIC, PNM, DPX, Cineon, IFF, OpenVDB, Ptex, Photoshop PSD, Wavefront RLA, SGI, WebP, GIF, DICOM, HEIF/HEIC/AVIF, many "RAW" digital camera formats, and a variety diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake index 6b21270b32..807c768a39 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -144,6 +144,12 @@ if (NOT TARGET libjpeg-turbo::jpeg) # Try to find the non-turbo version checked_find_package (JPEG REQUIRED) endif () +# JPEG XL +option (USE_JXL "Enable JPEG XL support" ON) +checked_find_package (JXL + VERSION_MIN 0.10.1 + DEFINITIONS -DUSE_JXL=1) + # Pugixml setup. Normally we just use the version bundled with oiio, but # some linux distros are quite particular about having separate packages so we # allow this to be overridden to use the distro-provided package if desired. diff --git a/src/cmake/modules/FindJXL.cmake b/src/cmake/modules/FindJXL.cmake new file mode 100644 index 0000000000..c1fd5f2e3e --- /dev/null +++ b/src/cmake/modules/FindJXL.cmake @@ -0,0 +1,45 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO +# +# Module to find libjxl +# +# Will define: +# - JXL_FOUND +# - JXL_INCLUDES directory to include for libjxl headers +# - JXL_LIBRARIES libraries to link to + +include (FindPackageHandleStandardArgs) + +find_path(JXL_INCLUDE_DIR + NAMES jxl/decode.h jxl/encode.h) +mark_as_advanced(JXL_INCLUDE_DIR) + +if (JXL_INCLUDE_DIR) + file (STRINGS "${JXL_INCLUDE_DIR}/jxl/version.h" TMP REGEX "^#define JPEGXL_MAJOR_VERSION .*$") + string (REGEX MATCHALL "[0-9]+" JPEGXL_MAJOR_VERSION ${TMP}) + file (STRINGS "${JXL_INCLUDE_DIR}/jxl/version.h" TMP REGEX "^#define JPEGXL_MINOR_VERSION .*$") + string (REGEX MATCHALL "[0-9]+" JPEGXL_MINOR_VERSION ${TMP}) + file (STRINGS "${JXL_INCLUDE_DIR}/jxl/version.h" TMP REGEX "^#define JPEGXL_PATCH_VERSION .*$") + string (REGEX MATCHALL "[0-9]+" JPEGXL_PATCH_VERSION ${TMP}) + set (JXL_VERSION "${JPEGXL_MAJOR_VERSION}.${JPEGXL_MINOR_VERSION}.${JPEGXL_PATCH_VERSION}") +endif () + +find_library(JXL_LIBRARY + NAMES jxl) +mark_as_advanced ( + JXL_LIBRARY + JXL_VERSION + ) + +find_library(JXL_THREADS_LIBRARY + NAMES jxl_threads) +mark_as_advanced(JXL_THREADS_LIBRARY) + +find_package_handle_standard_args(JXL + REQUIRED_VARS JXL_LIBRARY JXL_THREADS_LIBRARY JXL_INCLUDE_DIR) + +if(JXL_FOUND) + set(JXL_LIBRARIES ${JXL_LIBRARY} ${JXL_THREADS_LIBRARY}) + set(JXL_INCLUDES ${JXL_INCLUDE_DIR}) +endif(JXL_FOUND) diff --git a/src/iv/imageviewer.cpp b/src/iv/imageviewer.cpp index 9aa665645e..3da4e4f2a3 100644 --- a/src/iv/imageviewer.cpp +++ b/src/iv/imageviewer.cpp @@ -63,7 +63,7 @@ IsSpecSrgb(const ImageSpec& spec) // clang-format off static const char *s_file_filters = "" "Image Files (*.bmp *.cin *.dcm *.dds *.dpx *.fits *.gif *.hdr *.ico *.iff " - "*.jpg *.jpe *.jpeg *.jif *.jfif *.jfi *.jp2 *.j2k *.exr *.png *.pbm *.pgm " + "*.jpg *.jpe *.jpeg *.jif *.jfif *.jfi *.jp2 *.j2k *.jxl *.exr *.png *.pbm *.pgm " "*.ppm *.psd *.ptex *.rla *.sgi *.rgb *.rgba *.bw *.int *.inta *.pic *.tga " "*.tpic *.tif *.tiff *.tx *.env *.sm *.vsm *.vdb *.webp *.zfile);;" "BMP (*.bmp);;" @@ -78,6 +78,7 @@ static const char *s_file_filters = "" "IFF (*.iff);;" "JPEG (*.jpg *.jpe *.jpeg *.jif *.jfif *.jfi);;" "JPEG-2000 (*.jp2 *.j2k);;" + "JPEG XL (*.jxl);;" "OpenEXR (*.exr);;" "OpenVDB (*.vdb);;" "PhotoShop (*.psd);;" diff --git a/src/jpegxl.imageio/CMakeLists.txt b/src/jpegxl.imageio/CMakeLists.txt new file mode 100644 index 0000000000..5586b7cea5 --- /dev/null +++ b/src/jpegxl.imageio/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +if (JXL_FOUND) + add_oiio_plugin (jxlinput.cpp jxloutput.cpp + INCLUDE_DIRS ${JXL_INCLUDE_DIRS} + LINK_LIBRARIES ${JXL_LIBRARIES} + DEFINITIONS "-DUSE_JXL") +else() + message (WARNING "JPEG XL plugin will not be built") +endif() diff --git a/src/jpegxl.imageio/jxlinput.cpp b/src/jpegxl.imageio/jxlinput.cpp new file mode 100644 index 0000000000..036bacf2a0 --- /dev/null +++ b/src/jpegxl.imageio/jxlinput.cpp @@ -0,0 +1,366 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +// JPEG XL + +// https://jpeg.org/jpegxl/index.html +// https://jpegxl.info +// https://jpegxl.info/test-page +// https://people.csail.mit.edu/ericchan/hdr/hdr-jxl.php +// https://saklistudio.com/jxltests +// https://thorium.rocks +// https://bugs.chromium.org/p/chromium/issues/detail?id=1451807 + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +OIIO_PLUGIN_NAMESPACE_BEGIN + +#define DBG if (0) + +class JxlInput final : public ImageInput { +public: + JxlInput() { init(); } + ~JxlInput() override { close(); } + const char* format_name(void) const override { return "jpegxl"; } + int supports(string_view feature) const override + { + return (feature == "exif" || feature == "ioproxy"); + } + bool valid_file(Filesystem::IOProxy* ioproxy) const override; + + bool open(const std::string& name, ImageSpec& spec) override; + bool open(const std::string& name, ImageSpec& spec, + const ImageSpec& config) override; + bool read_native_scanline(int subimage, int miplevel, int y, int z, + void* data) override; + bool close() override; + + const std::string& filename() const { return m_filename; } + +private: + std::string m_filename; + int m_next_scanline; // Which scanline is the next to read? + uint32_t m_channels; + JxlDecoderPtr m_decoder; + JxlResizableParallelRunnerPtr m_runner; + std::unique_ptr m_config; // Saved copy of configuration spec + std::vector m_icc_profile; + std::vector m_pixels; + // std::vector m_pixels; + + void init() + { + ioproxy_clear(); + m_config.reset(); + m_decoder = nullptr; + m_runner = nullptr; + } + + void close_file() { init(); } +}; + + + +// Export version number and create function symbols +OIIO_PLUGIN_EXPORTS_BEGIN + +OIIO_EXPORT int jpegxl_imageio_version = OIIO_PLUGIN_VERSION; + + + +OIIO_EXPORT const char* +jpegxl_imageio_library_version() +{ + return "libjxl " OIIO_STRINGIZE(JPEGXL_MAJOR_VERSION) "." OIIO_STRINGIZE( + JPEGXL_MINOR_VERSION) "." OIIO_STRINGIZE(JPEGXL_PATCH_VERSION); +} + + + +OIIO_EXPORT ImageInput* +jpegxl_input_imageio_create() +{ + return new JxlInput; +} + +OIIO_EXPORT const char* jpegxl_input_extensions[] = { "jxl", nullptr }; + +OIIO_PLUGIN_EXPORTS_END + + + +bool +JxlInput::valid_file(Filesystem::IOProxy* ioproxy) const +{ + DBG std::cout << "JxlInput::valid_file()\n"; + + // Check magic number to assure this is a JPEG file + if (!ioproxy || ioproxy->mode() != Filesystem::IOProxy::Read) + return false; + + uint8_t magic[128] {}; + const size_t numRead = ioproxy->pread(magic, sizeof(magic), 0); + if (numRead != sizeof(magic)) + return false; + + JxlSignature signature = JxlSignatureCheck(magic, sizeof(magic)); + switch (signature) { + case JXL_SIG_CODESTREAM: + case JXL_SIG_CONTAINER: break; + default: return false; + } + + DBG std::cout << "JxlInput::valid_file() return true\n"; + return true; +} + + + +bool +JxlInput::open(const std::string& name, ImageSpec& newspec, + const ImageSpec& config) +{ + DBG std::cout << "JxlInput::open(name, newspec, config)\n"; + + ioproxy_retrieve_from_config(config); + m_config.reset(new ImageSpec(config)); // save config spec + return open(name, newspec); +} + + + +bool +JxlInput::open(const std::string& name, ImageSpec& newspec) +{ + DBG std::cout << "JxlInput::open(name, newspec)\n"; + + m_filename = name; + + DBG std::cout << "m_filename = " << m_filename << "\n"; + + if (!ioproxy_use_or_open(name)) { + DBG std::cout << "ioproxy_use_or_open returned false\n"; + return false; + } + + Filesystem::IOProxy* m_io = ioproxy(); + std::string proxytype = m_io->proxytype(); + if (proxytype != "file" && proxytype != "memreader") { + errorfmt("JPEG XL reader can't handle proxy type {}", proxytype); + return false; + } + + m_decoder = JxlDecoderMake(nullptr); + if (m_decoder == nullptr) { + DBG std::cout << "JxlDecoderMake failed\n"; + return false; + } + + m_runner = JxlResizableParallelRunnerMake(nullptr); + if (m_runner == nullptr) { + DBG std::cout << "JxlThreadParallelRunnerMake failed\n"; + return false; + } + + JxlDecoderStatus status = JxlDecoderSetParallelRunner( + m_decoder.get(), JxlResizableParallelRunner, m_runner.get()); + if (status != JXL_DEC_SUCCESS) { + DBG std::cout << "JxlDecoderSetParallelRunner failed\n"; + return false; + } + + status + = JxlDecoderSubscribeEvents(m_decoder.get(), + JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING + | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE); + if (status != JXL_DEC_SUCCESS) { + DBG std::cout << "JxlDecoderSubscribeEvents failed\n"; + return false; + } + + std::unique_ptr jxl; + + DBG std::cout << "proxytype = " << proxytype << "\n"; + if (proxytype == "file") { + size_t size = m_io->size(); + DBG std::cout << "size = " << size << "\n"; + jxl.reset(new uint8_t[size]); + size_t result = m_io->read(jxl.get(), size); + DBG std::cout << "result = " << result << "\n"; + + status = JxlDecoderSetInput(m_decoder.get(), jxl.get(), size); + if (status != JXL_DEC_SUCCESS) { + DBG std::cout << "JxlDecoderSetInput() returned " << status << "\n"; + return false; + } + JxlDecoderCloseInput(m_decoder.get()); + + } else { + auto buffer = reinterpret_cast(m_io)->buffer(); + status = JxlDecoderSetInput(m_decoder.get(), + const_cast(buffer.data()), + buffer.size()); + if (status != JXL_DEC_SUCCESS) { + return false; + } + } + + JxlBasicInfo info; + // JxlPixelFormat format = { channels, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0 }; + JxlPixelFormat format = { m_channels, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, + 0 }; + + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder.get()); + DBG std::cout << "JxlDecoderProcessInput() returned " << status << "\n"; + + if (status == JXL_DEC_ERROR) { + DBG std::cout << "JXL_DEC_ERROR\n"; + + errorfmt("JPEG XL decoder error"); + return false; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + DBG std::cout << "JXL_DEC_NEED_MORE_INPUT\n"; + + errorfmt("JPEG XL decoder error, already provided all input\n"); + return false; + } else if (status == JXL_DEC_BASIC_INFO) { + DBG std::cout << "JXL_DEC_BASIC_INFO\n"; + + if (JXL_DEC_SUCCESS + != JxlDecoderGetBasicInfo(m_decoder.get(), &info)) { + errorfmt("JxlDecoderGetBasicInfo failed\n"); + return false; + } + format.num_channels = info.num_color_channels + + info.num_extra_channels; + m_channels = info.num_color_channels + info.num_extra_channels; + JxlResizableParallelRunnerSetThreads( + m_runner.get(), + JxlResizableParallelRunnerSuggestThreads(info.xsize, + info.ysize)); + } else if (status == JXL_DEC_COLOR_ENCODING) { + DBG std::cout << "JXL_DEC_COLOR_ENCODING\n"; + + // Get the ICC color profile of the pixel data + size_t icc_size; + + if (JXL_DEC_SUCCESS + != JxlDecoderGetICCProfileSize(m_decoder.get(), + JXL_COLOR_PROFILE_TARGET_DATA, + &icc_size)) { + errorfmt("JxlDecoderGetICCProfileSize failed\n"); + return false; + } + m_icc_profile.resize(icc_size); + if (JXL_DEC_SUCCESS + != JxlDecoderGetColorAsICCProfile(m_decoder.get(), + JXL_COLOR_PROFILE_TARGET_DATA, + m_icc_profile.data(), + m_icc_profile.size())) { + errorfmt("JxlDecoderGetColorAsICCProfile failed\n"); + return false; + } + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + DBG std::cout << "JXL_DEC_NEED_IMAGE_OUT_BUFFER\n"; + + size_t buffer_size; + if (JXL_DEC_SUCCESS + != JxlDecoderImageOutBufferSize(m_decoder.get(), &format, + &buffer_size)) { + errorfmt("JxlDecoderImageOutBufferSize failed\n"); + return false; + } + if (buffer_size + != info.xsize * info.ysize * m_channels * sizeof(float)) { + errorfmt("Invalid out buffer size {} {}\n", buffer_size, + info.xsize * info.ysize * m_channels * sizeof(float)); + return false; + } + m_pixels.resize(info.xsize * info.ysize * m_channels); + void* pixels_buffer = (void*)m_pixels.data(); + size_t pixels_buffer_size = m_pixels.size() * sizeof(float); + // size_t pixels_buffer_size = m_pixels.size() * sizeof(uint8_t); + if (JXL_DEC_SUCCESS + != JxlDecoderSetImageOutBuffer(m_decoder.get(), &format, + pixels_buffer, + pixels_buffer_size)) { + errorfmt("JxlDecoderSetImageOutBuffer failed\n"); + return false; + } + } else if (status == JXL_DEC_FULL_IMAGE) { + DBG std::cout << "JXL_DEC_FULL_IMAGE\n"; + + // Nothing to do. Do not yet return. If the image is an animation, more + // full frames may be decoded. This example only keeps the last one. + } else if (status == JXL_DEC_FRAME) { + DBG std::cout << "JXL_DEC_FRAME\n"; + + } else if (status == JXL_DEC_SUCCESS) { + DBG std::cout << "JXL_DEC_SUCCESS\n"; + + // All decoding successfully finished. + // It's not required to call JxlDecoderReleaseInput(m_decoder.get()) here since + // the decoder will be destroyed. + break; + } else { + errorfmt("Unknown decoder status\n"); + return false; + } + } + + m_spec = ImageSpec(info.xsize, info.ysize, m_channels, TypeDesc::FLOAT); + // TypeDesc::UINT8); + newspec = m_spec; + return true; +} + + + +bool +JxlInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, + void* data) +{ + DBG std::cout << "JxlInput::read_native_scanline(, , " << y << ")\n"; + size_t scanline_size = m_spec.width * m_channels * sizeof(float); + // size_t scanline_size = m_spec.width * m_channels * sizeof(uint8_t); + + lock_guard lock(*this); + if (!seek_subimage(subimage, miplevel)) + return false; + if (y < 0 || y >= m_spec.height) // out of range scanline + return false; + + memcpy(data, (void*)((uint8_t*)(m_pixels.data()) + y * scanline_size), + scanline_size); + + return true; +} + + + +bool +JxlInput::close() +{ + DBG std::cout << "JxlInput::close()\n"; + + if (ioproxy_opened()) { + close_file(); + } + init(); // Reset to initial state + return true; +} + +OIIO_PLUGIN_NAMESPACE_END diff --git a/src/jpegxl.imageio/jxloutput.cpp b/src/jpegxl.imageio/jxloutput.cpp new file mode 100644 index 0000000000..0fbf324667 --- /dev/null +++ b/src/jpegxl.imageio/jxloutput.cpp @@ -0,0 +1,374 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +OIIO_PLUGIN_NAMESPACE_BEGIN + +#define DBG if (0) + +class JxlOutput final : public ImageOutput { +public: + JxlOutput() { init(); } + ~JxlOutput() override { close(); } + const char* format_name(void) const override { return "jpegxl"; } + int supports(string_view feature) const override + { + return (feature == "alpha" || feature == "nchannels" + || feature == "exif" || feature == "ioproxy" + || feature == "tiles"); + } + bool open(const std::string& name, const ImageSpec& spec, + OpenMode mode = Create) override; + bool write_scanline(int y, int z, TypeDesc format, const void* data, + stride_t xstride) override; + bool write_scanlines(int ybegin, int yend, int z, TypeDesc format, + const void* data, stride_t xstride = AutoStride, + stride_t ystride = AutoStride) override; + bool write_tile(int x, int y, int z, TypeDesc format, const void* data, + stride_t xstride, stride_t ystride, + stride_t zstride) override; + bool close() override; + +private: + std::string m_filename; + JxlEncoderPtr m_encoder; + JxlResizableParallelRunnerPtr m_runner; + JxlBasicInfo m_basic_info; + JxlEncoderFrameSettings* m_frame_settings; + JxlPixelFormat m_pixel_format; + + unsigned int m_dither; + std::vector m_scratch; + std::vector m_tilebuffer; + std::vector m_pixels; + + void init(void) + { + ioproxy_clear(); + m_encoder = nullptr; + m_runner = nullptr; + } + + bool save_image(); +}; + + + +OIIO_PLUGIN_EXPORTS_BEGIN + +OIIO_EXPORT ImageOutput* +jpegxl_output_imageio_create() +{ + return new JxlOutput; +} + + + +OIIO_EXPORT const char* jpegxl_output_extensions[] = { "jxl", nullptr }; + +OIIO_PLUGIN_EXPORTS_END + +bool +JxlOutput::open(const std::string& name, const ImageSpec& newspec, + OpenMode mode) +{ + JxlEncoderStatus status; + JxlEncoderError error; + + DBG std::cout << "JxlOutput::open(name, newspec, mode)\n"; + + // Save name and spec for later use + m_filename = name; + + if (!check_open(mode, newspec, + { 0, 1073741823, 0, 1073741823, 0, 1, 0, 4099 })) + return false; + + DBG std::cout << "m_filename = " << m_filename << "\n"; + + ioproxy_retrieve_from_config(m_spec); + if (!ioproxy_use_or_open(name)) { + DBG std::cout << "ioproxy_use_or_open returned false\n"; + return false; + } + + m_spec.set_format(TypeFloat); + + m_dither = (m_spec.format == TypeDesc::UINT8) + ? m_spec.get_int_attribute("oiio:dither", 0) + : 0; + + m_encoder = JxlEncoderMake(nullptr); + if (m_encoder == nullptr) { + DBG std::cout << "JxlEncoderMake failed\n"; + return false; + } + + JxlEncoderAllowExpertOptions(m_encoder.get()); + + const uint32_t threads + = JxlResizableParallelRunnerSuggestThreads(m_spec.width, m_spec.height); + + m_runner = JxlResizableParallelRunnerMake(nullptr); + if (m_runner == nullptr) { + DBG std::cout << "JxlThreadParallelRunnerMake failed\n"; + return false; + } + + JxlResizableParallelRunnerSetThreads(m_runner.get(), threads); + status = JxlEncoderSetParallelRunner(m_encoder.get(), + JxlResizableParallelRunner, + m_runner.get()); + + if (status != JXL_ENC_SUCCESS) { + error = JxlEncoderGetError(m_encoder.get()); + errorfmt("JxlEncoderSetParallelRunner failed with error {}", + (int)error); + return false; + } + + JxlEncoderInitBasicInfo(&m_basic_info); + + DBG std::cout << "m_spec " << m_spec.width << "×" << m_spec.height << "×" + << m_spec.nchannels << "\n"; + m_basic_info.xsize = m_spec.width; + m_basic_info.ysize = m_spec.height; + m_basic_info.bits_per_sample = 32; + // m_basic_info.exponent_bits_per_sample = 0; + m_basic_info.exponent_bits_per_sample = 8; + + if (m_spec.nchannels >= 4) { + m_basic_info.num_color_channels = 3; + m_basic_info.num_extra_channels = m_spec.nchannels - 3; + m_basic_info.alpha_bits = m_basic_info.bits_per_sample; + m_basic_info.alpha_exponent_bits = m_basic_info.exponent_bits_per_sample; + } else { + m_basic_info.num_color_channels = m_spec.nchannels; + } + + DBG std::cout << "m_basic_info " << m_basic_info.xsize << "×" + << m_basic_info.ysize << "×" + << m_basic_info.num_color_channels << "\n"; + + m_frame_settings = JxlEncoderFrameSettingsCreate(m_encoder.get(), nullptr); + + // const float quality = 100.0; + const int effort = 7; + const int tier = 0; + // Lossless only makes sense for integer modes + if (m_basic_info.exponent_bits_per_sample == 0) { + // Must preserve original profile for lossless mode + m_basic_info.uses_original_profile = JXL_TRUE; + JxlEncoderSetFrameDistance(m_frame_settings, 0.0); + JxlEncoderSetFrameLossless(m_frame_settings, JXL_TRUE); + } + + JxlEncoderFrameSettingsSetOption(m_frame_settings, + JXL_ENC_FRAME_SETTING_EFFORT, effort); + + JxlEncoderFrameSettingsSetOption(m_frame_settings, + JXL_ENC_FRAME_SETTING_DECODING_SPEED, + tier); + + // Codestream level should be chosen automatically given the settings + JxlEncoderSetBasicInfo(m_encoder.get(), &m_basic_info); + + if (m_basic_info.num_extra_channels > 0) { + for (int i = 0; i < m_basic_info.num_extra_channels; i++) { + JxlExtraChannelType type = JXL_CHANNEL_ALPHA; + JxlExtraChannelInfo extra_channel_info; + + JxlEncoderInitExtraChannelInfo(type, &extra_channel_info); + + extra_channel_info.bits_per_sample = m_basic_info.alpha_bits; + extra_channel_info.exponent_bits_per_sample + = m_basic_info.alpha_exponent_bits; + // extra_channel_info.alpha_premultiplied = premultiply; + + status = JxlEncoderSetExtraChannelInfo(m_encoder.get(), i, + &extra_channel_info); + if (status != JXL_ENC_SUCCESS) { + error = JxlEncoderGetError(m_encoder.get()); + errorfmt("JxlEncoderSetExtraChannelInfo failed with error {}", + (int)error); + return false; + } + } + } + + if (m_spec.tile_width && m_spec.tile_height) { + m_tilebuffer.resize(m_spec.image_bytes()); + } + + return true; +} + + + +bool +JxlOutput::write_scanline(int y, int z, TypeDesc format, const void* data, + stride_t xstride) +{ + DBG std::cout << "JxlOutput::write_scanline(y = " << y << " )\n"; + + return write_scanlines(y, y + 1, z, format, data, xstride, AutoStride); +} + + + +bool +JxlOutput::write_scanlines(int ybegin, int yend, int z, TypeDesc format, + const void* data, stride_t xstride, stride_t ystride) +{ + DBG std::cout << "JxlOutput::write_scanlines(ybegin = " << ybegin + << ", yend = " << yend << ", ...)\n"; + + stride_t zstride = AutoStride; + m_spec.auto_stride(xstride, ystride, zstride, format, m_spec.nchannels, + m_spec.width, m_spec.height); + size_t npixels = size_t(m_spec.width) * size_t(yend - ybegin); + size_t nvals = npixels * size_t(m_spec.nchannels); + + data = to_native_rectangle(m_spec.x, m_spec.x + m_spec.width, ybegin, yend, + z, z + 1, format, data, xstride, ystride, + zstride, m_scratch, m_dither, 0, ybegin, z); + + DBG std::cout << "data = " << data << " nvals = " << nvals << "\n"; + + std::vector::iterator m_it + = m_pixels.begin() + m_spec.width * ybegin * m_spec.nchannels; + + m_pixels.insert(m_it, (float*)data, (float*)data + nvals); + + return true; +} + + + +bool +JxlOutput::write_tile(int x, int y, int z, TypeDesc format, const void* data, + stride_t xstride, stride_t ystride, stride_t zstride) +{ + DBG std::cout << "JxlOutput::write_tile()\n"; + + // Emulate tiles by buffering the whole image + return copy_tile_to_image_buffer(x, y, z, format, data, xstride, ystride, + zstride, &m_tilebuffer[0]); +} + + + +bool +JxlOutput::save_image() +{ + JxlEncoderStatus status; + JxlEncoderError error; + std::vector compressed; + bool ok = true; + + DBG std::cout << "JxlOutput::save_image()\n"; + + m_pixel_format = { m_basic_info.num_color_channels + + m_basic_info.num_extra_channels, + JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0 }; + + const size_t pixels_size = m_basic_info.xsize * m_basic_info.ysize + * (m_basic_info.num_color_channels + + m_basic_info.num_extra_channels); + + m_pixels.resize(pixels_size); + + const void* data = m_pixels.data(); + size_t size = m_pixels.size() * sizeof(float); + + DBG std::cout << "data = " << data << " size = " << size << "\n"; + + status = JxlEncoderAddImageFrame(m_frame_settings, &m_pixel_format, data, + size); + DBG std::cout << "status = " << status << "\n"; + if (status != JXL_ENC_SUCCESS) { + error = JxlEncoderGetError(m_encoder.get()); + errorfmt("JxlEncoderAddImageFrame failed with error {}", (int)error); + return false; + } + + // No more image frames nor metadata boxes to add + DBG std::cout << "calling JxlEncoderCloseInput()\n"; + JxlEncoderCloseInput(m_encoder.get()); + + compressed.clear(); + compressed.resize(4096); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size() - (next_out - compressed.data()); + JxlEncoderStatus result = JXL_ENC_NEED_MORE_OUTPUT; + while (result == JXL_ENC_NEED_MORE_OUTPUT) { + DBG std::cout << "calling JxlEncoderProcessOutput()\n"; + result = JxlEncoderProcessOutput(m_encoder.get(), &next_out, + &avail_out); + DBG std::cout << "result = " << result << "\n"; + if (result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + compressed.resize(next_out - compressed.data()); + if (result != JXL_ENC_SUCCESS) { + DBG std::cout << "JxlEncoderProcessOutput failed.\n"; + return false; + } + + DBG std::cout << "compressed.size() = " << compressed.size() << "\n"; + + if (!iowrite(compressed.data(), 1, compressed.size())) { + DBG std::cout << "iowrite failed.\n"; + return false; + } + + DBG std::cout << "JxlOutput::save_image() return ok\n"; + return ok; +} + + + +bool +JxlOutput::close() +{ + bool ok = true; + + DBG std::cout << "JxlOutput::close()\n"; + + if (!ioproxy_opened()) { // Already closed + init(); + return true; + } + + if (m_spec.tile_width) { + // Handle tile emulation -- output the buffered pixels + OIIO_ASSERT(m_tilebuffer.size()); + ok &= write_scanlines(m_spec.y, m_spec.y + m_spec.height, 0, + m_spec.format, &m_tilebuffer[0]); + std::vector().swap(m_tilebuffer); + } + + save_image(); + + init(); + return ok; +} + +OIIO_PLUGIN_NAMESPACE_END diff --git a/src/libOpenImageIO/imageinout_test.cpp b/src/libOpenImageIO/imageinout_test.cpp index 8477a9194e..617de90385 100644 --- a/src/libOpenImageIO/imageinout_test.cpp +++ b/src/libOpenImageIO/imageinout_test.cpp @@ -218,6 +218,30 @@ test_write_proxy(string_view formatname, string_view extension, +bool +test_pixel_match(cspan a, cspan b, float eps = 1.0e-6f) +{ + if (a.size() != b.size()) + return false; + int printed = 0; + bool ok = true; + float maxdiff = 0.0f; + for (size_t i = 0, e = a.size(); i < e; ++i) { + float diff = fabsf(a[i] - b[i]); + if (diff > eps) { + maxdiff = std::max(maxdiff, diff); + ok = false; + if (printed++ < 16) + print("\t[{}] {} {}, diff = {}\n", i, a[i], b[i], diff); + } + } + if (!ok) + print("\tmax diff = {}\n", maxdiff); + return ok; +} + + + // Helper for test_all_formats: read the pixels of the given disk file into // a buffer, then use an IOProxy to read the "file" from the buffer, and // the pixels ought to match those of ImageBuf buf. @@ -230,6 +254,13 @@ test_read_proxy(string_view formatname, string_view extension, std::cout << " Reading Proxy " << formatname << " ... "; std::cout.flush(); + auto nvalues = oiio_span_size_type(buf.spec().image_pixels() + * buf.spec().nchannels); + float eps = 0.0f; + // Allow lossy formats to have a little more error + if (formatname == "heif" || formatname == "jpegxl") + eps = 0.001f; + // Read the disk file into readbuf as a blob -- just a byte-for-byte // copy of the file, but in memory. uint64_t bytes_written = Filesystem::file_size(disk_filename); @@ -244,8 +275,10 @@ test_read_proxy(string_view formatname, string_view extension, if (in) { std::vector readpixels; ok &= checked_read(in.get(), memname, readpixels, true); - ok &= memcmp(readpixels.data(), buf.localpixels(), readpixels.size()) - == 0; + OIIO_ASSERT(readpixels.size() == nvalues * sizeof(float)); + ok &= test_pixel_match({ (const float*)readpixels.data(), nvalues }, + { (const float*)buf.localpixels(), nvalues }, + eps); OIIO_CHECK_ASSERT( ok && "Read proxy with ImageInput didn't match original"); } else { @@ -266,9 +299,8 @@ test_read_proxy(string_view formatname, string_view extension, OIIO_ASSERT(buf.localpixels()); OIIO_CHECK_EQUAL(buf.spec().format, inbuf.spec().format); OIIO_CHECK_EQUAL(buf.spec().image_bytes(), inbuf.spec().image_bytes()); - ok2 &= memcmp(inbuf.localpixels(), buf.localpixels(), - buf.spec().image_bytes()) - == 0; + ok2 &= test_pixel_match({ (const float*)inbuf.localpixels(), nvalues }, + { (const float*)buf.localpixels(), nvalues }, eps); OIIO_CHECK_ASSERT(ok2 && "Read proxy with ImageBuf didn't match original"); ok &= ok2; @@ -321,6 +353,10 @@ test_all_formats() // Skip "formats" that aren't amenable to this kind of testing if (formatname == "null" || formatname == "term") continue; + float eps = 0.0f; + // Allow lossy formats to have a little more error + if (formatname == "heif" || formatname == "jpegxl") + eps = 0.001f; if (onlyformat.size() && formatname != onlyformat) continue; @@ -364,10 +400,14 @@ test_all_formats() ok = checked_read(in.get(), filename, pixels); if (!ok) continue; - ok = memcmp(orig_pixels, pixels.data(), pixels.size()) == 0; - OIIO_CHECK_ASSERT(ok && "Failed read/write comparison"); + auto nvalues = oiio_span_size_type(buf.spec().image_pixels() + * buf.spec().nchannels); + ok = test_pixel_match({ orig_pixels, nvalues }, + { (const float*)pixels.data(), nvalues }, + eps); if (ok) std::cout << term.ansi("green", "OK\n"); + OIIO_CHECK_ASSERT(ok && "Failed read/write comparison"); } else { (void)OIIO::geterror(); // discard error } diff --git a/src/libOpenImageIO/imageioplugin.cpp b/src/libOpenImageIO/imageioplugin.cpp index a51081cb46..46eadfdcf4 100644 --- a/src/libOpenImageIO/imageioplugin.cpp +++ b/src/libOpenImageIO/imageioplugin.cpp @@ -280,6 +280,7 @@ PLUGENTRY(ico); PLUGENTRY(iff); PLUGENTRY(jpeg); PLUGENTRY(jpeg2000); +PLUGENTRY(jpegxl); PLUGENTRY(null); PLUGENTRY(openexr); PLUGENTRY(openvdb); @@ -382,6 +383,9 @@ catalog_builtin_plugins() #if defined(USE_OPENJPEG) && !defined(DISABLE_JPEG2000) DECLAREPLUG (jpeg2000); #endif +#if defined(USE_JXL) + DECLAREPLUG (jpegxl); +#endif #if !defined(DISABLE_NULL) DECLAREPLUG (null); #endif