diff --git a/.ci/Brewfile b/.ci/Brewfile index a02908698..cc3709671 100644 --- a/.ci/Brewfile +++ b/.ci/Brewfile @@ -2,5 +2,7 @@ brew 'cmake' brew 'git' brew 'libomp' brew 'jpeg-turbo' +brew 'jpeg-xl' brew 'ninja' +brew 'pkgconf' brew 'pugixml' diff --git a/.ci/oss-fuzz.sh b/.ci/oss-fuzz.sh index 1ad14cb69..c72c6772b 100755 --- a/.ci/oss-fuzz.sh +++ b/.ci/oss-fuzz.sh @@ -62,7 +62,7 @@ cd build cmake \ -DBINARY_PACKAGE_BUILD=ON -DWITH_OPENMP=$WITH_OPENMP \ -DUSE_BUNDLED_LLVMOPENMP=ON -DALLOW_DOWNLOADING_LLVMOPENMP=ON \ - -DWITH_PUGIXML=OFF -DUSE_XMLLINT=OFF -DWITH_JPEG=OFF -DWITH_ZLIB=OFF \ + -DWITH_PUGIXML=OFF -DUSE_XMLLINT=OFF -DWITH_JPEG=OFF -DWITH_JXL=OFF -DWITH_ZLIB=OFF \ -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF -DBUILD_BENCHMARKING=OFF \ -DCMAKE_BUILD_TYPE=FUZZ -DBUILD_FUZZERS=ON \ -DLIB_FUZZING_ENGINE:STRING="$LIB_FUZZING_ENGINE" \ diff --git a/.github/workflows/CI-linux.yml b/.github/workflows/CI-linux.yml index d8afdde62..6b44db361 100644 --- a/.github/workflows/CI-linux.yml +++ b/.github/workflows/CI-linux.yml @@ -135,9 +135,11 @@ jobs: googletest \ binutils-gold \ libjpeg-dev \ + libjxl-dev \ libpugixml-dev \ libxml2-utils \ ninja-build \ + pkgconf \ zlib1g-dev if [ "$COMPILER_FAMILY" = "GNU" ]; then eatmydata apt install g++-${{ inputs.compiler-version }} \ diff --git a/.github/workflows/CI-windows-msys2.yml b/.github/workflows/CI-windows-msys2.yml index 17e0e8555..b06f949fe 100644 --- a/.github/workflows/CI-windows-msys2.yml +++ b/.github/workflows/CI-windows-msys2.yml @@ -60,6 +60,7 @@ jobs: libxml2:p pugixml:p libjpeg-turbo:p + libjxl:p zlib:p - name: Install Additional Dependencies (Coverage) if: inputs.flavor == 'Coverage' diff --git a/CMakeLists.txt b/CMakeLists.txt index 2346db8d4..11f823ce5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,7 @@ else() set(ALLOW_DOWNLOADING_PUGIXML OFF CACHE BOOL "If pugixml src tree is not found in location specified by PUGIXML_PATH, do fetch the archive from internet" FORCE) endif() option(WITH_JPEG "Enable JPEG support for DNG Lossy JPEG support" ON) +option(WITH_JXL "Enable JPEG XL support for DNG JPEG XL support" ON) option(WITH_ZLIB "Enable ZLIB support for DNG deflate support" ON) if(WITH_ZLIB) option(USE_BUNDLED_ZLIB "Build and use zlib in-tree" OFF) diff --git a/cmake/Modules/FindJXL.cmake b/cmake/Modules/FindJXL.cmake new file mode 100644 index 000000000..73d52cb9a --- /dev/null +++ b/cmake/Modules/FindJXL.cmake @@ -0,0 +1,33 @@ +# Find libjxl +# Will define: +# - JXL_FOUND +# - JXL_INCLUDE_DIRS directory to include for libjxl headers +# - JXL_LIBRARIES libraries to link to +# - JXL_VERSION + +find_package(PkgConfig QUIET REQUIRED) +pkg_check_modules(JXL_PKGCONF QUIET libjxl) + +if(JXL_PKGCONF_VERSION) + set(JXL_VERSION ${JXL_PKGCONF_VERSION}) +endif() + +find_path(JXL_INCLUDE_DIR + NAMES jxl/decode.h + HINTS ${JXL_PKGCONF_INCLUDE_DIRS}) +mark_as_advanced(JXL_INCLUDE_DIR) + +find_library(JXL_LIBRARY + NAMES jxl + HINTS ${JXL_PKGCONF_LIBRARY_DIRS}) +mark_as_advanced(JXL_LIBRARY) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(JXL + REQUIRED_VARS JXL_LIBRARY JXL_INCLUDE_DIR + VERSION_VAR JXL_VERSION) + +if(JXL_FOUND) + set(JXL_INCLUDE_DIRS ${JXL_INCLUDE_DIR}) + set(JXL_LIBRARIES ${JXL_LIBRARY}) +endif() diff --git a/cmake/src-dependencies.cmake b/cmake/src-dependencies.cmake index a9a887e6d..1529d483c 100644 --- a/cmake/src-dependencies.cmake +++ b/cmake/src-dependencies.cmake @@ -184,6 +184,33 @@ else() endif() add_feature_info("Lossy JPEG decoding" HAVE_JPEG "used for DNG Lossy JPEG compression decoding") +unset(HAVE_JXL) +if(WITH_JXL) + message(STATUS "Looking for JPEG XL") + find_package(JXL) + if(NOT JXL_FOUND) + message(SEND_ERROR "Did not find JPEG XL! Either make it find JPEG XL, or pass -DWITH_JXL=OFF to disable JPEG XL.") + else() + message(STATUS "Looking for JPEG XL - found") + set(HAVE_JXL 1) + + if(NOT TARGET JXL::jxl) + add_library(JXL::jxl INTERFACE IMPORTED) + set_property(TARGET JXL::jxl PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${JXL_INCLUDE_DIRS}") + set_property(TARGET JXL::jxl PROPERTY INTERFACE_LINK_LIBRARIES "${JXL_LIBRARIES}") + endif() + + target_link_libraries(rawspeed PRIVATE JXL::jxl) + set_package_properties(JXL PROPERTIES + TYPE RECOMMENDED + DESCRIPTION "library for handling the JPEG XL image data format, implements a JPEG XL codec" + PURPOSE "Used for decoding DNG JPEG XL compression") + endif() +else() + message(STATUS "JPEG XL is disabled, DNG JPEG XL support won't be available.") +endif() +add_feature_info("JPEG XL decoding" HAVE_JXL "used for DNG JPEG XL compression decoding") + unset(HAVE_ZLIB) if (WITH_ZLIB) message(STATUS "Looking for ZLIB") diff --git a/src/config.h.in b/src/config.h.in index 623d14174..7dec96d8a 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -65,6 +65,8 @@ static_assert(RAWSPEED_LARGEPAGESIZE >= RAWSPEED_PAGESIZE, #cmakedefine HAVE_JPEG #cmakedefine HAVE_JPEG_MEM_SRC +#cmakedefine HAVE_JXL + #cmakedefine HAVE_CXX_THREAD_LOCAL #cmakedefine HAVE_GCC_THREAD_LOCAL diff --git a/src/librawspeed/decoders/DngDecoder.cpp b/src/librawspeed/decoders/DngDecoder.cpp index ecd119898..8f7b0731b 100644 --- a/src/librawspeed/decoders/DngDecoder.cpp +++ b/src/librawspeed/decoders/DngDecoder.cpp @@ -54,6 +54,7 @@ #include using std::map; +using std::pair; using std::vector; namespace rawspeed { @@ -119,6 +120,9 @@ void DngDecoder::dropUnsuportedChunks(std::vector* data) { case 9: // VC-5 as used by GoPro #ifdef HAVE_JPEG case 0x884c: // lossy JPEG +#endif +#ifdef HAVE_JXL + case 0xcd42: // JPEG XL #endif // no change, if supported, then is still supported. break; @@ -140,6 +144,15 @@ void DngDecoder::dropUnsuportedChunks(std::vector* data) { "chunk, but the jpeg support was " "disabled at build!"); [[clang::fallthrough]]; +#endif +#ifndef HAVE_JXL + case 0xcd42: // JPEG XL +#pragma message \ + "JPEG XL is not present! JPEG XL compression will not be supported!" + writeLog(DEBUG_PRIO::WARNING, "DNG Decoder: found JPEG XL encoded " + "chunk, but the jpeg xl support was " + "disabled at build!"); + [[clang::fallthrough]]; #endif default: supported = false; @@ -362,16 +375,24 @@ void DngDecoder::decodeData(const TiffIFD* raw, uint32_t sample_format) const { if (compression == 8 && sample_format != 3) { ThrowRDE("Only float format is supported for " "deflate-compressed data."); - } else if ((compression == 7 || compression == 0x884c) && + } else if ((compression == 7 || compression == 0x884c || + compression == 0xcd42) && sample_format != 1) { ThrowRDE("Only 16 bit unsigned data supported for " - "JPEG-compressed data."); + "JPEG or JPEG XL compressed data."); } uint32_t predictor = ~0U; if (raw->hasEntry(TiffTag::PREDICTOR)) predictor = raw->getEntry(TiffTag::PREDICTOR)->getU32(); + pair interleave{1U, 1U}; + if (raw->hasEntry(TiffTag::ROWINTERLEAVEFACTOR)) + interleave.first = raw->getEntry(TiffTag::ROWINTERLEAVEFACTOR)->getU32(); + if (raw->hasEntry(TiffTag::COLUMNINTERLEAVEFACTOR)) + interleave.second = + raw->getEntry(TiffTag::COLUMNINTERLEAVEFACTOR)->getU32(); + if (mRaw->getDataType() == RawImageType::UINT16) { // Default white level is (2 ** BitsPerSample) - 1 mRaw->whitePoint = implicit_cast((1UL << *bps) - 1UL); @@ -401,7 +422,7 @@ void DngDecoder::decodeData(const TiffIFD* raw, uint32_t sample_format) const { } AbstractDngDecompressor slices(mRaw, getTilingDescription(raw), compression, - mFixLjpeg, *bps, predictor); + mFixLjpeg, *bps, predictor, interleave); slices.slices.reserve(slices.dsc.numTiles); diff --git a/src/librawspeed/decompressors/AbstractDngDecompressor.cpp b/src/librawspeed/decompressors/AbstractDngDecompressor.cpp index b828a4fe2..b625ecfa9 100644 --- a/src/librawspeed/decompressors/AbstractDngDecompressor.cpp +++ b/src/librawspeed/decompressors/AbstractDngDecompressor.cpp @@ -49,6 +49,10 @@ #include "decompressors/JpegDecompressor.h" #endif +#ifdef HAVE_JXL +#include "decompressors/JpegXLDecompressor.h" +#endif + namespace rawspeed { template <> void AbstractDngDecompressor::decompressThread<1>() const noexcept { @@ -201,6 +205,30 @@ void AbstractDngDecompressor::decompressThread<0x884c>() const noexcept { } #endif +#ifdef HAVE_JXL +template <> +void AbstractDngDecompressor::decompressThread<0xcd42>() const noexcept { +#ifdef HAVE_OPENMP +#pragma omp for schedule(static) +#endif + for (const auto& e : + Array1DRef(slices.data(), implicit_cast(slices.size()))) { + try { + JpegXLDecompressor j(e.bs.peekBuffer(e.bs.getRemainSize()), mRaw, + mInterleave); + j.decode(e.offX, e.offY); + } catch (const RawDecoderException& err) { + mRaw->setError(err.what()); + } catch (const IOException& err) { + mRaw->setError(err.what()); + } catch (...) { + // We should not get any other exception type here. + __builtin_unreachable(); + } + } +} +#endif + void AbstractDngDecompressor::decompressThread() const noexcept { invariant(mRaw->dim.x > 0); invariant(mRaw->dim.y > 0); @@ -232,6 +260,14 @@ void AbstractDngDecompressor::decompressThread() const noexcept { #else #pragma message "JPEG is not present! Lossy JPEG DNG will not be supported!" mRaw->setError("jpeg support is disabled."); +#endif + } else if (compression == 0xcd42) { + /* Lossy DNG */ +#ifdef HAVE_JXL + decompressThread<0xcd42>(); +#else +#pragma message "JPEG XL is not present! JPEG XL DNG will not be supported!" + mRaw->setError("jpeg xl support is disabled."); #endif } else { mRaw->setError("AbstractDngDecompressor: Unknown compression"); diff --git a/src/librawspeed/decompressors/AbstractDngDecompressor.h b/src/librawspeed/decompressors/AbstractDngDecompressor.h index dcd6ad0b9..d956c2ef5 100644 --- a/src/librawspeed/decompressors/AbstractDngDecompressor.h +++ b/src/librawspeed/decompressors/AbstractDngDecompressor.h @@ -134,9 +134,11 @@ class AbstractDngDecompressor final : public AbstractDecompressor { public: AbstractDngDecompressor(RawImage img, const DngTilingDescription& dsc_, int compression_, bool mFixLjpeg_, uint32_t mBps_, - uint32_t mPredictor_) + uint32_t mPredictor_, + std::pair mInterleave_) : mRaw(std::move(img)), dsc(dsc_), compression(compression_), - mFixLjpeg(mFixLjpeg_), mBps(mBps_), mPredictor(mPredictor_) {} + mFixLjpeg(mFixLjpeg_), mBps(mBps_), mPredictor(mPredictor_), + mInterleave(std::move(mInterleave_)) {} void decompress() const; @@ -148,6 +150,7 @@ class AbstractDngDecompressor final : public AbstractDecompressor { const bool mFixLjpeg = false; const uint32_t mBps; const uint32_t mPredictor; + const std::pair mInterleave; }; } // namespace rawspeed diff --git a/src/librawspeed/decompressors/CMakeLists.txt b/src/librawspeed/decompressors/CMakeLists.txt index 8933a3ad1..23863c5b4 100644 --- a/src/librawspeed/decompressors/CMakeLists.txt +++ b/src/librawspeed/decompressors/CMakeLists.txt @@ -26,6 +26,8 @@ FILE(GLOB SOURCES "JpegDecompressor.cpp" "JpegDecompressor.h" "JpegMarkers.h" + "JpegXLDecompressor.cpp" + "JpegXLDecompressor.h" "KodakDecompressor.cpp" "KodakDecompressor.h" "LJpegDecoder.cpp" @@ -82,4 +84,8 @@ if(WITH_JPEG AND TARGET JPEG::JPEG) target_link_libraries(rawspeed_decompressors PUBLIC JPEG::JPEG) endif() +if(WITH_JXL AND TARGET JXL::jxl) + target_link_libraries(rawspeed_decompressors PUBLIC JXL::jxl) +endif() + target_link_libraries(rawspeed PRIVATE rawspeed_decompressors) diff --git a/src/librawspeed/decompressors/JpegXLDecompressor.cpp b/src/librawspeed/decompressors/JpegXLDecompressor.cpp new file mode 100644 index 000000000..ac7fbf352 --- /dev/null +++ b/src/librawspeed/decompressors/JpegXLDecompressor.cpp @@ -0,0 +1,171 @@ +/* + RawSpeed - RAW file decoder. + + Copyright (C) 2009-2014 Klaus Post + Copyright (C) 2017 Roman Lebedev + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "rawspeedconfig.h" // IWYU pragma: keep + +#ifdef HAVE_JXL + +#include "adt/Array2DRef.h" +#include "decoders/RawDecoderException.h" +#include "decompressors/JpegXLDecompressor.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using std::min; + +namespace rawspeed { + +void JpegXLDecompressor::decode( + uint32_t offX, uint32_t offY) { /* Each slice is a JPEG XL image */ + + JxlSignature signature = JxlSignatureCheck(input.begin(), input.getSize()); + + if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) + ThrowRDE("Unable to verify JPEG XL signature"); + + if (mInterleave != std::pair(1U, 1U) && mInterleave != std::pair(2U, 2U)) + ThrowRDE("Invalid interleave factors"); + + if (mInterleave == std::pair(2U, 2U) && mRaw->getCpp() != 1) + ThrowRDE( + "Invalid combination of interleave factors and components per pixel"); + + JxlDecoder* decoder = JxlDecoderCreate(nullptr); + + if (!decoder) + ThrowRDE("Unable to instantiate a JPEG XL decoder"); + + if (JxlDecoderSetInput(decoder, input.begin(), input.getSize()) != + JXL_DEC_SUCCESS) { + JxlDecoderDestroy(decoder); + ThrowRDE("Unable to set input data for JPEG XL decoder"); + } + + if (JxlDecoderSubscribeEvents(decoder, + JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE) != + JXL_DEC_SUCCESS) { + JxlDecoderDestroy(decoder); + ThrowRDE("Unable to subscribe to JPEG XL decoder events"); + } + + JxlDecoderStatus status; + JxlBasicInfo basicinfo; + + const JxlPixelFormat pixel_format = { + mRaw->getCpp(), // number of channels + JXL_TYPE_UINT16, // data type + JXL_NATIVE_ENDIAN, // endianness + 0 // alignment + }; + + std::vector complete_buffer; + + // Decoding loop + while (true) { + status = JxlDecoderProcessInput(decoder); + + if (status == JXL_DEC_ERROR) { + JxlDecoderDestroy(decoder); + ThrowRDE("JPEG XL decoding error"); + } + + if (status == JXL_DEC_NEED_MORE_INPUT) { + JxlDecoderDestroy(decoder); + ThrowRDE("JPEG XL stream input data incomplete"); + } + + if (status == JXL_DEC_BASIC_INFO) { + if (JxlDecoderGetBasicInfo(decoder, &basicinfo) != JXL_DEC_SUCCESS) { + JxlDecoderDestroy(decoder); + ThrowRDE("JPEG XL stream basic info not available"); + } + + // Unlikely to happen, but let there be a sanity check + if (basicinfo.xsize == 0 || basicinfo.ysize == 0) { + JxlDecoderDestroy(decoder); + ThrowRDE("JPEG XL image declares zero dimensions"); + } + + if (basicinfo.num_color_channels != pixel_format.num_channels) + ThrowRDE("Component count doesn't match"); + + continue; // go to next loop iteration to process rest of the input + } + + if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + size_t size = + basicinfo.xsize * basicinfo.ysize * basicinfo.num_color_channels; + complete_buffer.resize(size); + JxlDecoderSetImageOutBuffer(decoder, &pixel_format, + complete_buffer.data(), size); + continue; // go to next iteration to process rest of the input + } + + // If the image is an animation, more full frames may be decoded. We do not + // check and reject the image if it is an animation, but only read the first + // frame. It hardly makes sense to process such an image. + if (status == JXL_DEC_FULL_IMAGE) + break; // Terminate processing + + } // end of processing loop + + JxlDecoderDestroy(decoder); + + const Array2DRef tmp(complete_buffer.data(), + basicinfo.num_color_channels * basicinfo.xsize, + basicinfo.ysize); + + // Now the image is decoded, and we copy the image data + unsigned int copy_w = min(mRaw->dim.x - offX, basicinfo.xsize); + unsigned int copy_h = min(mRaw->dim.y - offY, basicinfo.ysize); + + const Array2DRef out(mRaw->getU16DataAsUncroppedArray2DRef()); + for (unsigned int row = 0; row < copy_h; row++) { + unsigned int rowbuf = + (row / mInterleave.first) + + (row % mInterleave.first) * (basicinfo.ysize / mInterleave.first); + for (unsigned int col = 0; col < basicinfo.num_color_channels * copy_w; + col++) { + unsigned int colbuf = + (col / mInterleave.second) + + (col % mInterleave.second) * + ((basicinfo.num_color_channels * basicinfo.xsize) / + mInterleave.second); + out(offY + row, basicinfo.num_color_channels * offX + col) = + tmp(rowbuf, colbuf); + } + } +} + +} // namespace rawspeed + +#else + +#pragma message \ + "JPEG XL is not present! JPEG XL compression will not be supported!" + +#endif diff --git a/src/librawspeed/decompressors/JpegXLDecompressor.h b/src/librawspeed/decompressors/JpegXLDecompressor.h new file mode 100644 index 000000000..6423e1ac2 --- /dev/null +++ b/src/librawspeed/decompressors/JpegXLDecompressor.h @@ -0,0 +1,55 @@ +/* + RawSpeed - RAW file decoder. + + Copyright (C) 2017 Roman Lebedev + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "rawspeedconfig.h" + +#ifdef HAVE_JXL + +#include "common/RawImage.h" +#include "decompressors/AbstractDecompressor.h" +#include "io/Buffer.h" +#include +#include + +namespace rawspeed { + +class JpegXLDecompressor final : public AbstractDecompressor { + Buffer input; + RawImage mRaw; + std::pair mInterleave; + +public: + JpegXLDecompressor(Buffer bs, RawImage img, + std::pair interleave) + : input(bs), mRaw(std::move(img)), mInterleave(std::move(interleave)) {} + + void decode(uint32_t offsetX, uint32_t offsetY); +}; + +} // namespace rawspeed + +#else + +#pragma message \ + "JPEG XL is not present! JPEG XL compression will not be supported!" + +#endif diff --git a/src/librawspeed/tiff/TiffTag.h b/src/librawspeed/tiff/TiffTag.h index b4d20750a..1387db2d9 100644 --- a/src/librawspeed/tiff/TiffTag.h +++ b/src/librawspeed/tiff/TiffTag.h @@ -333,6 +333,7 @@ enum class TiffTag : uint16_t { OPCODELIST2 = 0xC741, OPCODELIST3 = 0xC742, NOISEPROFILE = 0xC761, + COLUMNINTERLEAVEFACTOR = 0xCD43, CANONCR2SLICE = 0xC640, // CANON CR2 CANON_SRAWTYPE = 0xC6C5, // IFD3 CANON_SRAWQUALITY = 0x002E,