diff --git a/CMakeLists.txt b/CMakeLists.txt index 051d215e2..5ed90ca61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -552,6 +552,11 @@ if(WITH_SYSTEM_KLT) find_package(KLT REQUIRED) endif() +pkg_check_modules(JXL IMPORTED_TARGET libjxl libjxl_threads) +if(JXL) + set(JXL_LIBRARIES jxl jxl_threads) +endif() + # Check for libcanberra-gtk3 (sound events on Linux): if(UNIX AND (NOT APPLE)) option(USE_LIBCANBERRA "Build with libcanberra" ON) diff --git a/rtdata/options/options.lin b/rtdata/options/options.lin index fbd8bc6cb..0f31df6ee 100644 --- a/rtdata/options/options.lin +++ b/rtdata/options/options.lin @@ -12,8 +12,8 @@ MultiUser=true [File Browser] # Image filename extensions to be looked for, and their corresponding search state (0/1 -> skip/include) -ParseExtensions=3fr;arw;arq;cr2;cr3;crf;crw;dcr;dng;fff;iiq;jpg;jpeg;kdc;mef;mos;mrw;nef;nrw;orf;ori;pef;png;raf;raw;rw2;rwl;rwz;sr2;srf;srw;tif;tiff;x3f; -ParseExtensionsEnabled=1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;0;1;0;1;1;1;1;1;1;1;1;1;1;1; +ParseExtensions=3fr;arw;arq;cr2;cr3;crf;crw;dcr;dng;fff;iiq;jpg;jpeg;kdc;mef;mos;mrw;nef;nrw;orf;ori;pef;png;raf;raw;rw2;rwl;rwz;sr2;srf;srw;tif;tiff;x3f;jxl; +ParseExtensionsEnabled=1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;0;1;0;1;1;1;1;1;1;1;1;1;1;1;1; [Output] PathTemplate=%p1/converted/%f diff --git a/rtdata/options/options.osx b/rtdata/options/options.osx index ef68d7144..273b189e5 100644 --- a/rtdata/options/options.osx +++ b/rtdata/options/options.osx @@ -13,8 +13,8 @@ UseSystemTheme=false [File Browser] # Image filename extensions to be looked for, and their corresponding search state (0/1 -> skip/include) -ParseExtensions=3fr;arw;arq;cr2;cr3;crf;crw;dcr;dng;fff;iiq;jpg;jpeg;kdc;mef;mos;mrw;nef;nrw;orf;ori;pef;png;raf;raw;rw2;rwl;rwz;sr2;srf;srw;tif;tiff;x3f; -ParseExtensionsEnabled=1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;0;1;0;1;1;1;1;1;1;1;1;1;1;1; +ParseExtensions=3fr;arw;arq;cr2;cr3;crf;crw;dcr;dng;fff;iiq;jpg;jpeg;kdc;mef;mos;mrw;nef;nrw;orf;ori;pef;png;raf;raw;rw2;rwl;rwz;sr2;srf;srw;tif;tiff;x3f;jxl; +ParseExtensionsEnabled=1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;0;1;0;1;1;1;1;1;1;1;1;1;1;1;1; [Output] PathTemplate=%p1/converted/%f diff --git a/rtdata/options/options.win b/rtdata/options/options.win index 00b74d07f..9f72a1a72 100644 --- a/rtdata/options/options.win +++ b/rtdata/options/options.win @@ -14,8 +14,8 @@ UseSystemTheme=false [File Browser] # Image filename extensions to be looked for, and their corresponding search state (0/1 -> skip/include) -ParseExtensions=3fr;arw;arq;cr2;cr3;crf;crw;dcr;dng;fff;iiq;jpg;jpeg;kdc;mef;mos;mrw;nef;nrw;orf;ori;pef;png;raf;raw;rw2;rwl;rwz;sr2;srf;srw;tif;tiff;x3f; -ParseExtensionsEnabled=1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;0;1;0;1;1;1;1;1;1;1;1;1;1;1; +ParseExtensions=3fr;arw;arq;cr2;cr3;crf;crw;dcr;dng;fff;iiq;jpg;jpeg;kdc;mef;mos;mrw;nef;nrw;orf;ori;pef;png;raf;raw;rw2;rwl;rwz;sr2;srf;srw;tif;tiff;x3f;jxl; +ParseExtensionsEnabled=1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;0;1;0;1;1;1;1;1;1;1;1;1;1;1;1; [Output] PathTemplate=%p1/converted/%f diff --git a/rtengine/CMakeLists.txt b/rtengine/CMakeLists.txt index 6f329f7be..b92917ef5 100644 --- a/rtengine/CMakeLists.txt +++ b/rtengine/CMakeLists.txt @@ -246,6 +246,7 @@ target_link_libraries(rtengine ${RSVG_LIBRARIES} ${KLT_LIBRARIES} ${EXIV2_LIBRARIES} + ${JXL_LIBRARIES} ) if(OpenMP_FOUND) diff --git a/rtengine/imageio.cc b/rtengine/imageio.cc index e2c6c1310..ece182821 100644 --- a/rtengine/imageio.cc +++ b/rtengine/imageio.cc @@ -24,13 +24,20 @@ #include #include #include - #include #include #include #include #include +#ifdef JXL +#include +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/resizable_parallel_runner.h" +#include "jxl/resizable_parallel_runner_cxx.h" +#endif + #ifdef _WIN32 #include #else @@ -822,6 +829,161 @@ int ImageIO::loadTIFF (const Glib::ustring &fname) return IMIO_SUCCESS; } +#ifdef JXL +#define _PROFILE_ JXL_COLOR_PROFILE_TARGET_ORIGINAL + +// adapted from libjxl +int ImageIO::loadJxl(const Glib::ustring &fname) +{ + if (pl) { + pl->setProgressStr("PROGRESSBAR_LOADJXL"); + pl->setProgress(0.0); + } + + std::vector icc_profile; + + gpointer buffer = nullptr; + size_t buffer_size = 0; + + JxlBasicInfo info = {}; + JxlPixelFormat format = {}; + + format.num_channels = 3; + format.data_type = JXL_TYPE_FLOAT; + format.endianness = JXL_NATIVE_ENDIAN; + format.align = 0; + + // get file contents + std::ifstream instream(fname.c_str(), std::ios::in | std::ios::binary); + std::vector compressed( + (std::istreambuf_iterator(instream)), + std::istreambuf_iterator()); + instream.close(); + + + // multi-threaded parallel runner. + auto runner = JxlResizableParallelRunnerMake(nullptr); + + auto dec = JxlDecoderMake(nullptr); + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)) { + g_printerr("Error: JxlDecoderSubscribeEvents failed\n"); + return false; + } + + if (JXL_DEC_SUCCESS != + JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, + runner.get())) { + g_printerr("Error: JxlDecoderSetParallelRunner failed\n"); + return false; + } + + // grand decode loop... + JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); + + while (true) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + + if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != + JxlDecoderGetBasicInfo(dec.get(), &info)) { + g_printerr("Error: JxlDecoderGetBasicInfo failed\n"); + return false; + } + + JxlResizableParallelRunnerSetThreads( + runner.get(), JxlResizableParallelRunnerSuggestThreads( + info.xsize, info.ysize)); + } else if (status == JXL_DEC_COLOR_ENCODING) { + // check for ICC profile + deleteLoadedProfileData(); + embProfile = nullptr; + size_t icc_size = 0; + + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize(dec.get(), &format, + _PROFILE_, &icc_size)) { + g_printerr("Warning: JxlDecoderGetICCProfileSize failed\n"); + } + + if (icc_size > 0) { + icc_profile.resize(icc_size); + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile( + dec.get(), &format, _PROFILE_, + icc_profile.data(), icc_profile.size())) { + g_printerr( + "Warning: JxlDecoderGetColorAsICCProfile failed\n"); + } else { + embProfile = cmsOpenProfileFromMem(icc_profile.data(), + icc_profile.size()); + } + } else { + g_printerr("Warning: Empty ICC data.\n"); + } + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + format.data_type = JXL_TYPE_FLOAT; + if (JXL_DEC_SUCCESS != JxlDecoderImageOutBufferSize( + dec.get(), &format, &buffer_size)) { + g_printerr("Error: JxlDecoderImageOutBufferSize failed\n"); + return false; + } + buffer = g_malloc(buffer_size); + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec.get(), &format, + buffer, buffer_size)) { + g_printerr("Error: JxlDecoderSetImageOutBuffer failed\n"); + g_free(buffer); + return false; + } + } else if (status == JXL_DEC_FULL_IMAGE || + status == JXL_DEC_FRAME) { + // Nothing to do. If the image is an animation, more full frames + // may be decoded. This example only keeps the first one. + } else if (status == JXL_DEC_SUCCESS) { + // All decoding successfully finished. + // It's not required to call JxlDecoderReleaseInput(dec.get()) + // since the decoder will be destroyed. + break; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + g_printerr("Error: Already provided all input\n"); + return false; + } else if (status == JXL_DEC_ERROR) { + g_printerr("Error: Decoder error\n"); + return false; + } else { + g_printerr("Error: Unknown decoder status\n"); + return false; + } + } // end grand decode loop + + unsigned int width = info.xsize; + unsigned int height = info.ysize; + + allocate(width, height); + + int line_length = width * 3 * 4; + + for (size_t row = 0; row < height; ++row) { + setScanline (row, ((const unsigned char*)buffer) + (row * line_length), 32); + + if (pl && !(row % 100)) { + pl->setProgress ((double)(row) / height); + } + } + + g_free(buffer); + + if (pl) { + pl->setProgressStr ("PROGRESSBAR_READY"); + pl->setProgress (1.0); + } + + return IMIO_SUCCESS; +} +#endif + int ImageIO::loadPPMFromMemory(const char* buffer, int width, int height, bool swap, int bps) { allocate (width, height); @@ -1310,6 +1472,10 @@ int ImageIO::load (const Glib::ustring &fname) return loadPNG (fname); } else if (hasJpegExtension(fname)) { return loadJPEG (fname); +#ifdef JXL + } else if (hasJxlExtension(fname)) { + return loadJxl(fname); +#endif } else if (hasTiffExtension(fname)) { return loadTIFF (fname); } else { diff --git a/rtengine/imageio.h b/rtengine/imageio.h index 813bfcc61..e7fa23b20 100644 --- a/rtengine/imageio.h +++ b/rtengine/imageio.h @@ -90,6 +90,10 @@ public: int load (const Glib::ustring &fname); int save (const Glib::ustring &fname) const; +#ifdef JXL + int loadJxl (const Glib::ustring &fname); +#endif + int loadPNG (const Glib::ustring &fname); int loadJPEG (const Glib::ustring &fname); int loadTIFF (const Glib::ustring &fname); diff --git a/rtengine/stdimagesource.cc b/rtengine/stdimagesource.cc index 435f2f9a0..33d02e5b1 100644 --- a/rtengine/stdimagesource.cc +++ b/rtengine/stdimagesource.cc @@ -90,6 +90,12 @@ void StdImageSource::getSampleFormat (const Glib::ustring &fname, IIOSampleForma if (result == IMIO_SUCCESS) { return; } +#ifdef JXL + } else if (hasJxlExtension(fname)) { + sFormat = IIOSF_FLOAT32; + sArrangement = IIOSA_CHUNKY; + return; +#endif } else if (hasTiffExtension(fname)) { int result = ImageIO::getTIFFSampleFormat (fname, sFormat, sArrangement); diff --git a/rtengine/utils.cc b/rtengine/utils.cc index 0674c9806..0a53f0a83 100644 --- a/rtengine/utils.cc +++ b/rtengine/utils.cc @@ -237,6 +237,14 @@ bool hasJpegExtension(const Glib::ustring& filename) return extension == "jpg" || extension == "jpeg"; } +#ifdef JXL +bool hasJxlExtension(const Glib::ustring& filename) +{ + const Glib::ustring extension = getFileExtension(filename); + return extension == "jxl"; +} +#endif + bool hasTiffExtension(const Glib::ustring& filename) { const Glib::ustring extension = getFileExtension(filename); diff --git a/rtengine/utils.h b/rtengine/utils.h index e0097d76e..06f42237c 100644 --- a/rtengine/utils.h +++ b/rtengine/utils.h @@ -52,6 +52,11 @@ bool hasTiffExtension(const Glib::ustring& filename); // Return true if file has .png extension (ignoring case) bool hasPngExtension(const Glib::ustring& filename); +#ifdef JXL +// Return true if file has .jxl extension (ignoring case) +bool hasJxlExtension(const Glib::ustring& filename); +#endif + void swab(const void* from, void* to, ssize_t n); } diff --git a/rtgui/main-cli.cc b/rtgui/main-cli.cc index 6d63d194a..6a3c34694 100644 --- a/rtgui/main-cli.cc +++ b/rtgui/main-cli.cc @@ -707,7 +707,7 @@ int processLineParams ( int argc, char **argv ) isRaw = true; Glib::ustring ext = getExtension (inputFile); - if (ext.lowercase() == "jpg" || ext.lowercase() == "jpeg" || ext.lowercase() == "tif" || ext.lowercase() == "tiff" || ext.lowercase() == "png") { + if (ext.lowercase() == "jpg" || ext.lowercase() == "jpeg" || ext.lowercase() == "tif" || ext.lowercase() == "tiff" || ext.lowercase() == "png" || ext.lowercase() == "jxl") { isRaw = false; } diff --git a/rtgui/thumbnail.cc b/rtgui/thumbnail.cc index 266dbacd3..a0164bc7d 100644 --- a/rtgui/thumbnail.cc +++ b/rtgui/thumbnail.cc @@ -212,6 +212,12 @@ void Thumbnail::_generateThumbnailImage () if (tpp) { cfs.format = FT_Jpeg; } + } else if (ext == "jxl") { + tpp = rtengine::Thumbnail::loadFromImage (fname, tw, th, -1, pparams->wb.equal); + + if (tpp) { + cfs.format = FT_Png; + } } else if (ext == "png") { tpp = rtengine::Thumbnail::loadFromImage (fname, tw, th, -1, pparams->wb.equal, pparams->wb.observer); @@ -255,6 +261,15 @@ void Thumbnail::_generateThumbnailImage () } } + if (!tpp) { + // try a custom loader + tpp = rtengine::Thumbnail::loadFromImage(fname, tw, th, -1, pparams->wb.equal, true); + if (tpp) { + cfs.format = FT_Custom; + infoFromImage(fname); + } + } + if (tpp) { tpp->getAutoWBMultipliers(cfs.redAWBMul, cfs.greenAWBMul, cfs.blueAWBMul); _saveThumbnail ();