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
This commit is contained in:
Kumataro 2025-01-27 23:18:28 +09:00 committed by GitHub
parent ae57c54d83
commit c840e24e94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 29 deletions

View File

@ -28,13 +28,15 @@ static void cbRGBAtoGRAY_32F(void *opaque, size_t x, size_t y, size_t num_pixels
/////////////////////// JpegXLDecoder /////////////////// /////////////////////// 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_signature = "\xFF\x0A";
m_decoder = nullptr; m_decoder = nullptr;
m_buf_supported = false; m_buf_supported = true;
m_type = -1; m_type = -1;
m_status = JXL_DEC_NEED_MORE_INPUT; m_status = JXL_DEC_NEED_MORE_INPUT;
m_is_mbuf_set = false;
} }
JpegXLDecoder::~JpegXLDecoder() JpegXLDecoder::~JpegXLDecoder()
@ -52,6 +54,7 @@ void JpegXLDecoder::close()
m_width = m_height = 0; m_width = m_height = 0;
m_type = -1; m_type = -1;
m_status = JXL_DEC_NEED_MORE_INPUT; 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 // 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() bool JpegXLDecoder::readHeader()
{ {
// Open file if (m_buf.empty()) {
if (!m_f) { // Open file
m_f.reset(fopen(m_filename.c_str(), "rb")); if (!m_f) {
if (!m_f) m_f.reset(fopen(m_filename.c_str(), "rb"));
return false; if (!m_f) {
return false;
}
}
} }
// Initialize decoder // Initialize decoder
@ -119,6 +125,9 @@ bool JpegXLDecoder::readHeader()
} }
} }
// Reset to read header data stream
m_is_mbuf_set = false;
return read(); return read();
} }
@ -195,38 +204,53 @@ bool JpegXLDecoder::readData(Mat& img)
// Common reading routine for readHeader() and readBody() // Common reading routine for readHeader() and readBody()
bool JpegXLDecoder::read() 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 // Start decoding loop
do { do {
// Check if we need more input // Check if we need more input
if (m_status == JXL_DEC_NEED_MORE_INPUT) { if (m_status == JXL_DEC_NEED_MORE_INPUT) {
size_t remaining = JxlDecoderReleaseInput(m_decoder.get()); uint8_t* data_ptr = nullptr;
// Move any remaining bytes to the beginning size_t data_len = 0;
if (remaining > 0)
memmove(m_read_buffer.data(), m_read_buffer.data() + m_read_buffer.size() - remaining, remaining); if( !m_buf.empty() ) {
// Read more data from file // When data source in on memory
size_t bytes_read = fread(m_read_buffer.data() + remaining, if (m_is_mbuf_set) {
1, m_read_buffer.size() - remaining, m_f.get()); // We expect m_buf contains whole JpegXL data stream.
if (bytes_read == 0) { // If it had been truncated, m_status will be JXL_DEC_NEED_MORE_INPUT again.
if (ferror(m_f.get())) { CV_LOG_WARNING(NULL, "Truncated JXL data in memory");
CV_LOG_WARNING(NULL, "Error reading input file");
return false; return false;
} }
// If we reached EOF but decoder needs more input, file is truncated data_ptr = m_buf.ptr();
if (m_status == JXL_DEC_NEED_MORE_INPUT) { data_len = m_buf.total();
CV_LOG_WARNING(NULL, "Truncated JXL file"); m_is_mbuf_set = true;
return false; }
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 // Set input buffer
if (JXL_DEC_SUCCESS != JxlDecoderSetInput(m_decoder.get(), // It must be kept until calling JxlDecoderReleaseInput() or m_decoder.reset().
m_read_buffer.data(), if (JXL_DEC_SUCCESS != JxlDecoderSetInput(m_decoder.get(), data_ptr, data_len)) {
bytes_read + remaining)) {
return false; return false;
} }
} }

View File

@ -43,6 +43,7 @@ protected:
JxlPixelFormat m_format; JxlPixelFormat m_format;
std::vector<uint8_t> m_read_buffer; std::vector<uint8_t> m_read_buffer;
JxlDecoderStatus m_status; JxlDecoderStatus m_status;
bool m_is_mbuf_set;
private: private:
bool read(); bool read();

View File

@ -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<uint8_t> buff;
vector<int> 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<uint8_t> buff;
vector<int> 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 #endif // HAVE_JPEGXL