From c840e24e9488d11d88ebd81a7d658efe05be8224 Mon Sep 17 00:00:00 2001 From: Kumataro Date: Mon, 27 Jan 2025 23:18:28 +0900 Subject: [PATCH] Merge pull request #26844 from Kumataro:fix26843 imgcodecs: jpegxl: imdecode() directly read from memory #26844 Close #26843 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake --- modules/imgcodecs/src/grfmt_jpegxl.cpp | 82 +++++++++++++++++--------- modules/imgcodecs/src/grfmt_jpegxl.hpp | 1 + modules/imgcodecs/test/test_jpegxl.cpp | 57 ++++++++++++++++++ 3 files changed, 111 insertions(+), 29 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_jpegxl.cpp b/modules/imgcodecs/src/grfmt_jpegxl.cpp index d94a722178..060174eb02 100644 --- a/modules/imgcodecs/src/grfmt_jpegxl.cpp +++ b/modules/imgcodecs/src/grfmt_jpegxl.cpp @@ -28,13 +28,15 @@ static void cbRGBAtoGRAY_32F(void *opaque, size_t x, size_t y, size_t num_pixels /////////////////////// JpegXLDecoder /////////////////// -JpegXLDecoder::JpegXLDecoder() : m_f(nullptr, &fclose) +JpegXLDecoder::JpegXLDecoder() : m_f(nullptr, &fclose), + m_read_buffer(16384,0) // 16KB chunks { m_signature = "\xFF\x0A"; m_decoder = nullptr; - m_buf_supported = false; + m_buf_supported = true; m_type = -1; m_status = JXL_DEC_NEED_MORE_INPUT; + m_is_mbuf_set = false; } JpegXLDecoder::~JpegXLDecoder() @@ -52,6 +54,7 @@ void JpegXLDecoder::close() m_width = m_height = 0; m_type = -1; m_status = JXL_DEC_NEED_MORE_INPUT; + m_is_mbuf_set = false; } // see https://github.com/libjxl/libjxl/blob/v0.10.0/doc/format_overview.md @@ -91,11 +94,14 @@ ImageDecoder JpegXLDecoder::newDecoder() const bool JpegXLDecoder::readHeader() { - // Open file - if (!m_f) { - m_f.reset(fopen(m_filename.c_str(), "rb")); - if (!m_f) - return false; + if (m_buf.empty()) { + // Open file + if (!m_f) { + m_f.reset(fopen(m_filename.c_str(), "rb")); + if (!m_f) { + return false; + } + } } // Initialize decoder @@ -119,6 +125,9 @@ bool JpegXLDecoder::readHeader() } } + // Reset to read header data stream + m_is_mbuf_set = false; + return read(); } @@ -195,38 +204,53 @@ bool JpegXLDecoder::readData(Mat& img) // Common reading routine for readHeader() and readBody() bool JpegXLDecoder::read() { - // Create buffer for reading - const size_t read_buffer_size = 16384; // 16KB chunks - if (m_read_buffer.capacity() < read_buffer_size) - m_read_buffer.resize(read_buffer_size); - // Start decoding loop do { // Check if we need more input if (m_status == JXL_DEC_NEED_MORE_INPUT) { - size_t remaining = JxlDecoderReleaseInput(m_decoder.get()); - // Move any remaining bytes to the beginning - if (remaining > 0) - memmove(m_read_buffer.data(), m_read_buffer.data() + m_read_buffer.size() - remaining, remaining); - // Read more data from file - size_t bytes_read = fread(m_read_buffer.data() + remaining, - 1, m_read_buffer.size() - remaining, m_f.get()); - if (bytes_read == 0) { - if (ferror(m_f.get())) { - CV_LOG_WARNING(NULL, "Error reading input file"); + uint8_t* data_ptr = nullptr; + size_t data_len = 0; + + if( !m_buf.empty() ) { + // When data source in on memory + if (m_is_mbuf_set) { + // We expect m_buf contains whole JpegXL data stream. + // If it had been truncated, m_status will be JXL_DEC_NEED_MORE_INPUT again. + CV_LOG_WARNING(NULL, "Truncated JXL data in memory"); return false; } - // If we reached EOF but decoder needs more input, file is truncated - if (m_status == JXL_DEC_NEED_MORE_INPUT) { - CV_LOG_WARNING(NULL, "Truncated JXL file"); - return false; + data_ptr = m_buf.ptr(); + data_len = m_buf.total(); + m_is_mbuf_set = true; + } + else { + // When data source is on file + // Release input buffer if it had been set already. If not, there are no errors. + size_t remaining = JxlDecoderReleaseInput(m_decoder.get()); + // Move any remaining bytes to the beginning + if (remaining > 0) + memmove(m_read_buffer.data(), m_read_buffer.data() + m_read_buffer.size() - remaining, remaining); + // Read more data from file + size_t bytes_read = fread(m_read_buffer.data() + remaining, + 1, m_read_buffer.size() - remaining, m_f.get()); + if (bytes_read == 0) { + if (ferror(m_f.get())) { + CV_LOG_WARNING(NULL, "Error reading input file"); + return false; + } + // If we reached EOF but decoder needs more input, file is truncated + if (m_status == JXL_DEC_NEED_MORE_INPUT) { + CV_LOG_WARNING(NULL, "Truncated JXL file"); + return false; + } } + data_ptr = m_read_buffer.data(); + data_len = bytes_read + remaining; } // Set input buffer - if (JXL_DEC_SUCCESS != JxlDecoderSetInput(m_decoder.get(), - m_read_buffer.data(), - bytes_read + remaining)) { + // It must be kept until calling JxlDecoderReleaseInput() or m_decoder.reset(). + if (JXL_DEC_SUCCESS != JxlDecoderSetInput(m_decoder.get(), data_ptr, data_len)) { return false; } } diff --git a/modules/imgcodecs/src/grfmt_jpegxl.hpp b/modules/imgcodecs/src/grfmt_jpegxl.hpp index e1b971a157..d9b046b779 100644 --- a/modules/imgcodecs/src/grfmt_jpegxl.hpp +++ b/modules/imgcodecs/src/grfmt_jpegxl.hpp @@ -43,6 +43,7 @@ protected: JxlPixelFormat m_format; std::vector m_read_buffer; JxlDecoderStatus m_status; + bool m_is_mbuf_set; private: bool read(); diff --git a/modules/imgcodecs/test/test_jpegxl.cpp b/modules/imgcodecs/test/test_jpegxl.cpp index 8a94faa42c..d43b625364 100644 --- a/modules/imgcodecs/test/test_jpegxl.cpp +++ b/modules/imgcodecs/test/test_jpegxl.cpp @@ -248,6 +248,63 @@ INSTANTIATE_TEST_CASE_P( ) ) ); +TEST(Imgcodecs_JpegXL, imdecode_truncated_stream) +{ + cv::Mat src(100, 100, CV_8UC1, Scalar(40,50,10)); + vector buff; + vector param; + + bool ret = false; + EXPECT_NO_THROW(ret = cv::imencode(".jxl", src, buff, param)); + EXPECT_TRUE(ret); + + // Try to decode non-truncated image. + cv::Mat decoded; + EXPECT_NO_THROW(decoded = cv::imdecode(buff, cv::IMREAD_COLOR)); + EXPECT_FALSE(decoded.empty()); + + // Try to decode truncated image. + buff.resize(buff.size() - 1 ); + EXPECT_NO_THROW(decoded = cv::imdecode(buff, cv::IMREAD_COLOR)); + EXPECT_TRUE(decoded.empty()); +} + +TEST(Imgcodecs_JpegXL, imread_truncated_stream) +{ + string tmp_fname = cv::tempfile(".jxl"); + cv::Mat src(100, 100, CV_8UC1, Scalar(40,50,10)); + vector buff; + vector param; + + bool ret = false; + EXPECT_NO_THROW(ret = cv::imencode(".jxl", src, buff, param)); + EXPECT_TRUE(ret); + + // Try to decode non-truncated image. + FILE *fp = nullptr; + + fp = fopen(tmp_fname.c_str(), "wb"); + EXPECT_TRUE(fp != nullptr); + fwrite(&buff[0], sizeof(uint8_t), buff.size(), fp); + fclose(fp); + + cv::Mat decoded; + EXPECT_NO_THROW(decoded = cv::imread(tmp_fname, cv::IMREAD_COLOR)); + EXPECT_FALSE(decoded.empty()); + + // Try to decode truncated image. + fp = fopen(tmp_fname.c_str(), "wb"); + EXPECT_TRUE(fp != nullptr); + fwrite(&buff[0], sizeof(uint8_t), buff.size() - 1, fp); + fclose(fp); + + EXPECT_NO_THROW(decoded = cv::imread(tmp_fname, cv::IMREAD_COLOR)); + EXPECT_TRUE(decoded.empty()); + + // Delete temporary file + remove(tmp_fname.c_str()); +} + #endif // HAVE_JPEGXL