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() : 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;
}
}

View File

@ -43,6 +43,7 @@ protected:
JxlPixelFormat m_format;
std::vector<uint8_t> m_read_buffer;
JxlDecoderStatus m_status;
bool m_is_mbuf_set;
private:
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