From 119dd753e9d85e2f5a440f19d73c76bdd9303e71 Mon Sep 17 00:00:00 2001 From: Ijtihed Kilani Date: Tue, 14 Jul 2026 07:22:23 +0300 Subject: [PATCH] imgcodecs(tiff): support reading images with more than 4 channels --- modules/imgcodecs/src/grfmt_tiff.cpp | 11 ++++-- modules/imgcodecs/test/test_tiff.cpp | 54 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_tiff.cpp b/modules/imgcodecs/src/grfmt_tiff.cpp index f47e4722bb..27bd0c56ed 100644 --- a/modules/imgcodecs/src/grfmt_tiff.cpp +++ b/modules/imgcodecs/src/grfmt_tiff.cpp @@ -236,8 +236,10 @@ bool TiffDecoder::checkSignature( const String& signature ) const int TiffDecoder::normalizeChannelsNumber(int channels) const { - CV_Check(channels, channels >= 1 && channels <= 4, "Unsupported number of channels"); - return channels; + CV_Check(channels, channels >= 1, "Unsupported number of channels"); + // TIFFs may have >4 samples per pixel (CMYK, CMYK + alpha, ...); libtiff's RGBA + // reader collapses them to 4 channels, so clamp instead of rejecting. + return std::min(channels, 4); } ImageDecoder TiffDecoder::newDecoder() const @@ -744,7 +746,10 @@ bool TiffDecoder::readData( Mat& img ) CV_Assert((int)tile_width0 > 0 && (int)tile_width0 <= TILE_MAX_WIDTH); CV_Assert((int)tile_height0 > 0 && (int)tile_height0 <= TILE_MAX_HEIGHT); const uint64_t MAX_TILE_SIZE = (CV_BIG_UINT(1) << 30); - CV_CheckLE((int)ncn, 4, ""); + // 8-bit images are read via libtiff's RGBA reader, which handles >4 samples + // per pixel; deeper depths read raw samples and support at most 4 channels. + if (dst_bpp != 8) + CV_CheckLE((int)ncn, 4, ""); CV_CheckLE((int)bpp, 64, ""); if (dst_bpp == 8) diff --git a/modules/imgcodecs/test/test_tiff.cpp b/modules/imgcodecs/test/test_tiff.cpp index 400e9a9a15..caf262edf6 100644 --- a/modules/imgcodecs/test/test_tiff.cpp +++ b/modules/imgcodecs/test/test_tiff.cpp @@ -1293,6 +1293,60 @@ TEST(Imgcodecs_Tiff, read_junk) { ASSERT_TRUE(img.empty()); } +TEST(Imgcodecs_Tiff, read_multi_channel_tiff_26771) +{ + // Minimal 4x4, 8-bit, uncompressed, contiguous TIFF with 5 samples per pixel + // (PHOTOMETRIC_SEPARATED / CMYK + one extra sample), reproducing issue #26771. + // libtiff collapses the samples to RGBA, so the image must be readable via the + // usual color/grayscale/unchanged paths instead of throwing on the channel count. + const unsigned char tiff[] = { + 0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x01, + 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x03, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x11, 0x01, + 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x15, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x16, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x17, 0x01, + 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x1c, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x52, 0x01, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x0a, 0xff, 0x28, 0x00, 0x14, 0x0a, 0xff, 0x50, 0x00, + 0x28, 0x0a, 0xff, 0x78, 0x00, 0x3c, 0x0a, 0xff, 0x00, 0x28, 0x14, 0x0a, + 0xff, 0x28, 0x28, 0x28, 0x0a, 0xff, 0x50, 0x28, 0x3c, 0x0a, 0xff, 0x78, + 0x28, 0x50, 0x0a, 0xff, 0x00, 0x50, 0x28, 0x0a, 0xff, 0x28, 0x50, 0x3c, + 0x0a, 0xff, 0x50, 0x50, 0x50, 0x0a, 0xff, 0x78, 0x50, 0x64, 0x0a, 0xff, + 0x00, 0x78, 0x3c, 0x0a, 0xff, 0x28, 0x78, 0x50, 0x0a, 0xff, 0x50, 0x78, + 0x64, 0x0a, 0xff, 0x78, 0x78, 0x78, 0x0a, 0xff + }; + const Mat buf(1, (int)sizeof(tiff), CV_8UC1, (void*)tiff); + + Mat color; + ASSERT_NO_THROW(color = imdecode(buf, IMREAD_COLOR)); + ASSERT_FALSE(color.empty()); + EXPECT_EQ(4, color.cols); + EXPECT_EQ(4, color.rows); + EXPECT_EQ(3, color.channels()); + + Mat gray; + ASSERT_NO_THROW(gray = imdecode(buf, IMREAD_GRAYSCALE)); + ASSERT_FALSE(gray.empty()); + EXPECT_EQ(Size(4, 4), gray.size()); + EXPECT_EQ(1, gray.channels()); + + Mat unchanged; + ASSERT_NO_THROW(unchanged = imdecode(buf, IMREAD_UNCHANGED)); + ASSERT_FALSE(unchanged.empty()); + EXPECT_EQ(Size(4, 4), unchanged.size()); + // A >4-channel TIFF is decoded via libtiff's RGBA reader, so the result has 4 channels. + EXPECT_EQ(4, unchanged.channels()); + double mn = 0.0, mx = 0.0; + minMaxLoc(unchanged, &mn, &mx); + EXPECT_LT(mn, mx); +} + typedef int Imgcodecs_Tiff_32F_Compressions_32F_Values; typedef testing::TestWithParam Imgcodecs_Tiff_32F_Compressions_32F;