From ff6112c09ed2245b10b56f888f96a56ec2374e50 Mon Sep 17 00:00:00 2001 From: Adel-Ayoub Date: Mon, 20 Jul 2026 21:22:30 +0100 Subject: [PATCH] imgcodecs: fix decoding of TIFF images with separate planes --- modules/imgcodecs/src/grfmt_tiff.cpp | 171 ++++++++++- modules/imgcodecs/test/test_tiff.cpp | 420 +++++++++++++++++++++++++++ 2 files changed, 583 insertions(+), 8 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_tiff.cpp b/modules/imgcodecs/src/grfmt_tiff.cpp index f47e4722bb..cea901a7d5 100644 --- a/modules/imgcodecs/src/grfmt_tiff.cpp +++ b/modules/imgcodecs/src/grfmt_tiff.cpp @@ -383,7 +383,7 @@ bool TiffDecoder::readHeader() if( bpp > 8 && ((photometric > 2) || - (ncn != 1 && ncn != 3 && ncn != 4))) + (ncn != 1 && (ncn != 2 || !isGrayScale) && ncn != 3 && ncn != 4))) bpp = 8; uint16_t sample_format = SAMPLEFORMAT_UINT; @@ -677,6 +677,80 @@ static void _unpack14To16(const uchar* src, const uchar* srcEnd, ushort* dst, us } //end _unpack14To16() +// Reads one strip (or tile) per sample and interleaves the planes, so that the +// conversion code below sees the same pixel layout as for PLANARCONFIG_CONTIG. +// Packed 10/12/14-bit planes are unpacked here, per plane row. +static void readSeparatePlanesBand(TIFF* tif, int band_x, int band_y, int rows, + bool is_tiled, uint32_t tile_width, uint16_t ncn, + uint16_t bpp, int dst_bpp, uchar* dst, + size_t dst_bytes_per_row, uchar* plane_buffer, + size_t plane_buffer_size, size_t plane_bytes_per_row, + ushort* plane_row_unpacked) +{ + constexpr const int bitsPerByte = 8; + const bool needsUnpacking = bpp < dst_bpp; + const int elem_bytes = (needsUnpacking ? dst_bpp : (int)bpp) / bitsPerByte; + for (uint16_t sample = 0; sample < ncn; sample++) + { + if (is_tiled) + { + const uint32_t tile = TIFFComputeTile(tif, band_x, band_y, 0, sample); + CV_TIFF_CHECK_CALL((int)TIFFReadEncodedTile( + tif, tile, plane_buffer, plane_buffer_size) >= 0); + } + else + { + const uint32_t strip = TIFFComputeStrip(tif, band_y, sample); + CV_TIFF_CHECK_CALL((int)TIFFReadEncodedStrip( + tif, strip, plane_buffer, plane_buffer_size) >= 0); + } + for (int row = 0; row < rows; row++) + { + const uchar* src_row = plane_buffer + row * plane_bytes_per_row; + if (needsUnpacking) + { + if (bpp == 10) + _unpack10To16(src_row, src_row + plane_bytes_per_row, + plane_row_unpacked, + plane_row_unpacked + tile_width, tile_width); + else if (bpp == 12) + _unpack12To16(src_row, src_row + plane_bytes_per_row, + plane_row_unpacked, + plane_row_unpacked + tile_width, tile_width); + else if (bpp == 14) + _unpack14To16(src_row, src_row + plane_bytes_per_row, + plane_row_unpacked, + plane_row_unpacked + tile_width, tile_width); + src_row = (const uchar*)plane_row_unpacked; + } + uchar* dst_row = dst + row * dst_bytes_per_row; + if (elem_bytes == 2) + { + const ushort* s = (const ushort*)src_row; + ushort* d = (ushort*)dst_row; + for (uint32_t j = 0; j < tile_width; j++) + d[static_cast(j) * ncn + sample] = s[j]; + } + else if (elem_bytes == 4) + { + const uint32_t* s = (const uint32_t*)src_row; + uint32_t* d = (uint32_t*)dst_row; + for (uint32_t j = 0; j < tile_width; j++) + d[static_cast(j) * ncn + sample] = s[j]; + } + else + { + // 8-byte rows may be only 4-aligned on 32-bit platforms (AutoBuffer + // inline storage), so copy without a uint64_t* dereference + CV_DbgAssert(elem_bytes == 8); + for (uint32_t j = 0; j < tile_width; j++) + memcpy(dst_row + (static_cast(j) * ncn + sample) * 8, + src_row + static_cast(j) * 8, 8); + } + } + } +} + bool TiffDecoder::readData( Mat& img ) { int type = img.type(); @@ -708,6 +782,9 @@ bool TiffDecoder::readData( Mat& img ) bpp = 1; } CV_TIFF_CHECK_CALL_DEBUG(TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &ncn)); + uint16_t planar_config = PLANARCONFIG_CONTIG; + CV_TIFF_CHECK_CALL_DEBUG(TIFFGetField(tif, TIFFTAG_PLANARCONFIG, &planar_config)); + const bool is_planar = ncn > 1 && planar_config == PLANARCONFIG_SEPARATE; uint16_t img_orientation = ORIENTATION_TOPLEFT; CV_TIFF_CHECK_CALL_DEBUG(TIFFGetField(tif, TIFFTAG_ORIENTATION, &img_orientation)); constexpr const int bitsPerByte = 8; @@ -860,17 +937,65 @@ bool TiffDecoder::readData( Mat& img ) tile_height0 = 1; } - const size_t src_buffer_bytes_per_row = divUp(static_cast(ncn * tile_width0 * bpp), static_cast(bitsPerByte)); - const size_t src_buffer_size = tile_height0 * src_buffer_bytes_per_row; - CV_CheckLT(src_buffer_size, MAX_TILE_SIZE, "buffer_size is too large: >= 1Gb"); - const size_t src_buffer_unpacked_bytes_per_row = divUp(static_cast(ncn * tile_width0 * dst_bpp), static_cast(bitsPerByte)); - const size_t src_buffer_unpacked_size = tile_height0 * src_buffer_unpacked_bytes_per_row; + const uint64_t src_buffer_bits_per_row = + static_cast(ncn) * tile_width0 * bpp; + const uint64_t src_buffer_bytes_per_row64 = + (src_buffer_bits_per_row + bitsPerByte - 1) / bitsPerByte; + const uint64_t src_buffer_size64 = + static_cast(tile_height0) * src_buffer_bytes_per_row64; + if (src_buffer_size64 >= MAX_TILE_SIZE) + CV_Error(Error::StsError, "buffer_size is too large: >= 1Gb"); + const size_t src_buffer_bytes_per_row = static_cast(src_buffer_bytes_per_row64); + const size_t src_buffer_size = static_cast(src_buffer_size64); + const uint64_t src_buffer_unpacked_bits_per_row = + static_cast(ncn) * tile_width0 * dst_bpp; + const uint64_t src_buffer_unpacked_bytes_per_row64 = + (src_buffer_unpacked_bits_per_row + bitsPerByte - 1) / bitsPerByte; + const uint64_t src_buffer_unpacked_size64 = + static_cast(tile_height0) * src_buffer_unpacked_bytes_per_row64; + if (src_buffer_unpacked_size64 > + static_cast(std::numeric_limits::max())) + CV_Error(Error::StsError, "unpacked buffer size is too large"); + const size_t src_buffer_unpacked_bytes_per_row = + static_cast(src_buffer_unpacked_bytes_per_row64); + const size_t src_buffer_unpacked_size = + static_cast(src_buffer_unpacked_size64); const bool needsUnpacking = (bpp < dst_bpp); AutoBuffer _src_buffer(src_buffer_size); uchar* src_buffer = _src_buffer.data(); AutoBuffer _src_buffer_unpacked(needsUnpacking ? src_buffer_unpacked_size : 0); uchar* src_buffer_unpacked = needsUnpacking ? _src_buffer_unpacked.data() : nullptr; + // In PLANARCONFIG_SEPARATE files all strips (or tiles) of sample 0 are stored + // first, then all strips of sample 1, and so on (TIFF 6.0). The dst_bpp == 8 path + // is not affected because TIFFReadRGBA* handles both planar configurations. + const bool doReadSeparatePlanes = is_planar && dst_bpp > 8 && !doReadScanline; + const uint64_t plane_bits_per_row = static_cast(tile_width0) * bpp; + const uint64_t plane_bytes_per_row64 = + (plane_bits_per_row + bitsPerByte - 1) / bitsPerByte; + const uint64_t plane_buffer_size64 = + static_cast(tile_height0) * plane_bytes_per_row64; + if (plane_buffer_size64 > static_cast(std::numeric_limits::max())) + CV_Error(Error::StsError, "plane buffer size is too large"); + const size_t plane_bytes_per_row = static_cast(plane_bytes_per_row64); + const size_t plane_buffer_size = static_cast(plane_buffer_size64); + AutoBuffer _plane_buffer(doReadSeparatePlanes ? plane_buffer_size : 0); + uchar* plane_buffer = _plane_buffer.data(); + AutoBuffer _plane_row_unpacked( + doReadSeparatePlanes && needsUnpacking ? tile_width0 : 0); + ushort* plane_row_unpacked = _plane_row_unpacked.data(); + + if (doReadSeparatePlanes) + { + CV_CheckGE(plane_buffer_size, + static_cast(is_tiled ? TIFFTileSize(tif) : TIFFStripSize(tif)), + "plane buffer is smaller than libtiff strip/tile size"); + } + + uchar* separate_planes_dst = needsUnpacking ? src_buffer_unpacked : src_buffer; + const size_t separate_planes_dst_bytes_per_row = + needsUnpacking ? src_buffer_unpacked_bytes_per_row : src_buffer_bytes_per_row; + if ( doReadScanline ) { CV_CheckGE(src_buffer_size, @@ -1028,6 +1153,15 @@ bool TiffDecoder::readData( Mat& img ) { CV_TIFF_CHECK_CALL((int)TIFFReadScanline(tif, (uint32_t*)src_buffer, y) >= 0); } + else if (doReadSeparatePlanes) + { + readSeparatePlanesBand( + tif, x, y, tile_height, is_tiled, tile_width0, ncn, + bpp, dst_bpp, separate_planes_dst, + separate_planes_dst_bytes_per_row, plane_buffer, + plane_buffer_size, plane_bytes_per_row, + plane_row_unpacked); + } else if (!is_tiled) { CV_TIFF_CHECK_CALL((int)TIFFReadEncodedStrip(tif, tileidx, (uint32_t*)src_buffer, src_buffer_size) >= 0); @@ -1040,7 +1174,13 @@ bool TiffDecoder::readData( Mat& img ) for (int i = 0; i < tile_height; i++) { ushort* buffer16 = (ushort*)(src_buffer+i*src_buffer_bytes_per_row); - if (needsUnpacking) + if (needsUnpacking && doReadSeparatePlanes) + { + // readSeparatePlanesBand already unpacked while interleaving + buffer16 = (ushort*)(src_buffer_unpacked + + i * src_buffer_unpacked_bytes_per_row); + } + else if (needsUnpacking) { const uchar* src_packed = src_buffer+i*src_buffer_bytes_per_row; uchar* dst_unpacked = src_buffer_unpacked+i*src_buffer_unpacked_bytes_per_row; @@ -1108,6 +1248,12 @@ bool TiffDecoder::readData( Mat& img ) buffer16, tile_width*sizeof(ushort)); } + else if( ncn == 2 ) + { + ushort* dst = img.ptr(img_y + i, x); + for (int j = 0; j < tile_width; j++) + dst[j] = buffer16[j * 2]; + } else { icvCvt_BGRA2Gray_16u_CnC1R(buffer16, 0, @@ -1122,7 +1268,16 @@ bool TiffDecoder::readData( Mat& img ) case 32: case 64: { - if( !is_tiled ) + if( doReadSeparatePlanes ) + { + readSeparatePlanesBand( + tif, x, y, tile_height, is_tiled, tile_width0, ncn, + bpp, dst_bpp, separate_planes_dst, + separate_planes_dst_bytes_per_row, plane_buffer, + plane_buffer_size, plane_bytes_per_row, + plane_row_unpacked); + } + else if( !is_tiled ) { CV_TIFF_CHECK_CALL((int)TIFFReadEncodedStrip(tif, tileidx, src_buffer, src_buffer_size) >= 0); } diff --git a/modules/imgcodecs/test/test_tiff.cpp b/modules/imgcodecs/test/test_tiff.cpp index 400e9a9a15..f6ba222062 100644 --- a/modules/imgcodecs/test/test_tiff.cpp +++ b/modules/imgcodecs/test/test_tiff.cpp @@ -1334,6 +1334,426 @@ const int Imgcodecs_Tiff_32F_Compressions_32F_All_Values[] = INSTANTIATE_TEST_CASE_P(compressions_32F, Imgcodecs_Tiff_32F_Compressions_32F, testing::ValuesIn(Imgcodecs_Tiff_32F_Compressions_32F_All_Values)); +//================================================================================================== +// See https://github.com/opencv/opencv/issues/28717 +// In PLANARCONFIG_SEPARATE files all strips (or tiles) of the first sample are stored first, +// then all strips of the second sample, and so on. OpenCV always writes PLANARCONFIG_CONTIG, +// so the fixtures below are assembled byte by byte. + +static const uint16_t TIFF_PLANARCONFIG_CONTIG = 1; +static const uint16_t TIFF_PLANARCONFIG_SEPARATE = 2; + +static void putLE16(std::vector& buf, uint32_t v) +{ + buf.push_back((uchar)(v & 0xff)); + buf.push_back((uchar)((v >> 8) & 0xff)); +} + +static void putLE32(std::vector& buf, uint32_t v) +{ + putLE16(buf, v & 0xffff); + putLE16(buf, v >> 16); +} + +static void patchLE32(std::vector& buf, size_t pos, uint32_t v) +{ + for (int i = 0; i < 4; i++) + buf[pos + i] = (uchar)((v >> (8 * i)) & 0xff); +} + +static uint64_t sampleBitsAt(const Mat& img, int y, int x, int ch) +{ + const size_t esz = img.elemSize1(); + const uchar* p = img.ptr(y) + (static_cast(x) * img.channels() + ch) * esz; + if (esz == 1) + { + uchar v; + memcpy(&v, p, 1); + return v; + } + if (esz == 2) + { + ushort v; + memcpy(&v, p, 2); + return v; + } + if (esz == 4) + { + uint32_t v; + memcpy(&v, p, 4); + return v; + } + uint64_t v; + memcpy(&v, p, 8); + return v; +} + +// Appends one page to an uncompressed little-endian TIFF. The image is written in R,G,B(,A) +// sample order while img is B,G,R(,A). ifdLinkPos tracks the IFD chain across pages and must +// start at 0 with an empty file. +static void appendTiffPage(std::vector& file, size_t& ifdLinkPos, const Mat& img, + uint16_t planarConfig, int rowsPerStrip, Size tileSize = Size(), + int bitsOverride = 0) +{ + const int w = img.cols, h = img.rows, spp = img.channels(); + const bool tiled = tileSize.width > 0; + CV_Assert(tiled || rowsPerStrip > 0); + const int bits = bitsOverride ? bitsOverride : (int)(img.elemSize1() * 8); + const uint16_t sampleFormat = (img.depth() == CV_32F || img.depth() == CV_64F) ? 3 : 1; + const int planes = planarConfig == TIFF_PLANARCONFIG_SEPARATE ? spp : 1; + const int samplesPerBlockPixel = planarConfig == TIFF_PLANARCONFIG_SEPARATE ? 1 : spp; + + if (file.empty()) + { + file.push_back('I'); + file.push_back('I'); + putLE16(file, 42); + ifdLinkPos = file.size(); + putLE32(file, 0); + } + + const auto fileChannel = [spp](int s) + { + return spp >= 3 ? (s == 0 ? 2 : (s == 2 ? 0 : s)) : s; + }; + + // plane < 0 emits all samples interleaved (contiguous); rows are byte-aligned + const auto putRow = [&](int y, int x0, int cols, int plane) + { + std::vector vals; + for (int x = x0; x < x0 + cols; x++) + { + if (plane < 0) + for (int s = 0; s < spp; s++) + vals.push_back(sampleBitsAt(img, y, x, fileChannel(s))); + else + vals.push_back(sampleBitsAt(img, y, x, fileChannel(plane))); + } + if (bits % 8 == 0) + { + for (size_t i = 0; i < vals.size(); i++) + for (int b = 0; b < bits / 8; b++) + file.push_back((uchar)((vals[i] >> (8 * b)) & 0xff)); + } + else + { + uint32_t acc = 0; + int nbits = 0; + for (size_t i = 0; i < vals.size(); i++) + { + CV_Assert(vals[i] < ((uint64_t)1 << bits)); + acc = (acc << bits) | (uint32_t)vals[i]; + nbits += bits; + while (nbits >= 8) + { + nbits -= 8; + file.push_back((uchar)((acc >> nbits) & 0xff)); + } + } + if (nbits > 0) + file.push_back((uchar)((acc << (8 - nbits)) & 0xff)); + } + }; + + std::vector blockOffsets, blockCounts; + const auto alignEven = [&]() { if (file.size() % 2) file.push_back(0); }; + const auto beginBlock = [&]() { alignEven(); blockOffsets.push_back((uint32_t)file.size()); }; + const auto endBlock = [&]() + { + blockCounts.push_back((uint32_t)file.size() - blockOffsets.back()); + }; + const auto padZeros = [&](size_t n) { file.insert(file.end(), n, (uchar)0); }; + + if (tiled) + { + // packed rows can only be zero-padded at byte granularity, so packed tiled + // fixtures need the width to be a whole number of tiles + CV_Assert(bits % 8 == 0 || w % tileSize.width == 0); + const int tw = tileSize.width, th = tileSize.height; + const size_t fullRowBytes = (size_t)divUp(tw * samplesPerBlockPixel * bits, 8); + for (int plane = 0; plane < planes; plane++) + { + for (int ty = 0; ty < h; ty += th) + { + for (int tx = 0; tx < w; tx += tw) + { + beginBlock(); + const int cols = std::min(tw, w - tx); + const size_t colsBytes = (size_t)divUp(cols * samplesPerBlockPixel * bits, 8); + for (int row = 0; row < th; row++) + { + const int y = ty + row; + if (y < h) + { + putRow(y, tx, cols, + planarConfig == TIFF_PLANARCONFIG_SEPARATE ? plane : -1); + padZeros(fullRowBytes - colsBytes); + } + else + { + padZeros(fullRowBytes); + } + } + endBlock(); + } + } + } + } + else + { + for (int plane = 0; plane < planes; plane++) + { + for (int y0 = 0; y0 < h; y0 += rowsPerStrip) + { + beginBlock(); + for (int y = y0; y < std::min(h, y0 + rowsPerStrip); y++) + putRow(y, 0, w, planarConfig == TIFF_PLANARCONFIG_SEPARATE ? plane : -1); + endBlock(); + } + } + } + + const auto putShortArray = [&](const std::vector& v) -> uint32_t + { + alignEven(); + const uint32_t off = (uint32_t)file.size(); + for (size_t i = 0; i < v.size(); i++) + putLE16(file, v[i]); + return off; + }; + const auto putLongArray = [&](const std::vector& v) -> uint32_t + { + alignEven(); + const uint32_t off = (uint32_t)file.size(); + for (size_t i = 0; i < v.size(); i++) + putLE32(file, v[i]); + return off; + }; + + struct IfdEntry + { + uint16_t tag, type; + uint32_t count, value; + }; + std::vector entries; + const auto add = [&entries](uint16_t tag, uint16_t type, uint32_t count, uint32_t value) + { + IfdEntry e = {tag, type, count, value}; + entries.push_back(e); + }; + + add(256, 4, 1, (uint32_t)w); + add(257, 4, 1, (uint32_t)h); + if (spp == 1) + add(258, 3, 1, (uint32_t)bits); + else if (spp == 2) + add(258, 3, 2, (uint32_t)bits | ((uint32_t)bits << 16)); + else + add(258, 3, (uint32_t)spp, putShortArray(std::vector(spp, (uint16_t)bits))); + add(259, 3, 1, 1); + add(262, 3, 1, spp >= 3 ? 2 : 1); + const uint32_t nblocks = (uint32_t)blockOffsets.size(); + if (tiled) + { + add(322, 4, 1, (uint32_t)tileSize.width); + add(323, 4, 1, (uint32_t)tileSize.height); + add(324, 4, nblocks, nblocks == 1 ? blockOffsets[0] : putLongArray(blockOffsets)); + add(325, 4, nblocks, nblocks == 1 ? blockCounts[0] : putLongArray(blockCounts)); + } + else + { + add(273, 4, nblocks, nblocks == 1 ? blockOffsets[0] : putLongArray(blockOffsets)); + add(278, 4, 1, (uint32_t)rowsPerStrip); + add(279, 4, nblocks, nblocks == 1 ? blockCounts[0] : putLongArray(blockCounts)); + } + add(277, 3, 1, (uint32_t)spp); + add(284, 3, 1, planarConfig); + if (spp == 2 || spp == 4) + add(338, 3, 1, 2); // one extra sample, unassociated alpha + if (spp == 1) + add(339, 3, 1, sampleFormat); + else if (spp == 2) + add(339, 3, 2, (uint32_t)sampleFormat | ((uint32_t)sampleFormat << 16)); + else + add(339, 3, (uint32_t)spp, putShortArray(std::vector(spp, sampleFormat))); + + std::sort(entries.begin(), entries.end(), + [](const IfdEntry& a, const IfdEntry& b) { return a.tag < b.tag; }); + + alignEven(); + patchLE32(file, ifdLinkPos, (uint32_t)file.size()); + putLE16(file, (uint32_t)entries.size()); + for (size_t i = 0; i < entries.size(); i++) + { + putLE16(file, entries[i].tag); + putLE16(file, entries[i].type); + putLE32(file, entries[i].count); + putLE32(file, entries[i].value); + } + ifdLinkPos = file.size(); + putLE32(file, 0); +} + +static Mat makePlanarTestMat(int type, Size size) +{ + Mat img(size, type); + const int cn = img.channels(); + for (int y = 0; y < size.height; y++) + { + for (int x = 0; x < size.width; x++) + { + for (int c = 0; c < cn; c++) + { + const int seed = x * 619 + y * 131 + c * 21845; + switch (img.depth()) + { + case CV_8U: img.ptr(y)[x * cn + c] = (uchar)(seed % 256); break; + case CV_16U: img.ptr(y)[x * cn + c] = (ushort)(seed % 65536); break; + case CV_32F: + img.ptr(y)[x * cn + c] = (float)x + y * 0.25f + c * 1000.5f; + break; + case CV_64F: img.ptr(y)[x * cn + c] = x + y * 0.25 + c * 1000.5; break; + default: CV_Assert(0); + } + } + } + } + return img; +} + +typedef tuple PlanarSeparateParams; // rowsPerStrip > 0, or 0 for 16x16 tiles +typedef testing::TestWithParam Imgcodecs_Tiff_PlanarSeparate; + +TEST_P(Imgcodecs_Tiff_PlanarSeparate, decode_matches_contig) +{ + const int type = get<0>(GetParam()); + const int rowsPerStrip = get<1>(GetParam()); + const Size tileSize = rowsPerStrip > 0 ? Size() : Size(16, 16); + const Mat truth = makePlanarTestMat(type, Size(21, 13)); + + std::vector contig, separate; + size_t link = 0; + appendTiffPage(contig, link, truth, TIFF_PLANARCONFIG_CONTIG, rowsPerStrip, tileSize); + link = 0; + appendTiffPage(separate, link, truth, TIFF_PLANARCONFIG_SEPARATE, rowsPerStrip, tileSize); + + // the contiguous file also validates the fixture builder itself + Mat decodedContig = imdecode(contig, IMREAD_UNCHANGED); + ASSERT_PRED_FORMAT2(cvtest::MatComparator(0, 0), truth, decodedContig); + + Mat decodedSeparate = imdecode(separate, IMREAD_UNCHANGED); + ASSERT_FALSE(decodedSeparate.empty()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), truth, decodedSeparate); + + if (truth.depth() == CV_16U && truth.channels() >= 3) + { + Mat grayContig = imdecode(contig, IMREAD_ANYDEPTH | IMREAD_GRAYSCALE); + Mat graySeparate = imdecode(separate, IMREAD_ANYDEPTH | IMREAD_GRAYSCALE); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), grayContig, graySeparate); + } +} + +const perf::MatType planar_mat_types[] = { CV_16UC1, CV_16UC3, CV_16UC4, CV_32FC3, CV_64FC3 }; +// single-row strips, partial last strip, one strip, tiles +const int planar_layouts[] = { 1, 2, 13, 0 }; + +INSTANTIATE_TEST_CASE_P(Layouts, Imgcodecs_Tiff_PlanarSeparate, + testing::Combine( + testing::ValuesIn(planar_mat_types), + testing::ValuesIn(planar_layouts) + ) +); + +TEST(Imgcodecs_Tiff, decode_planar_separate_8bit) +{ + const Mat truth = makePlanarTestMat(CV_8UC3, Size(21, 13)); + std::vector separate; + size_t link = 0; + appendTiffPage(separate, link, truth, TIFF_PLANARCONFIG_SEPARATE, 4); + + Mat unchanged = imdecode(separate, IMREAD_UNCHANGED); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), truth, unchanged); + Mat color = imdecode(separate, IMREAD_COLOR); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), truth, color); +} + +TEST(Imgcodecs_Tiff, decode_planar_separate_gray_alpha_16bit) +{ + const Mat samples = makePlanarTestMat(CV_16UC2, Size(21, 13)); + Mat expected; + extractChannel(samples, expected, 0); + + std::vector separate; + size_t link = 0; + appendTiffPage(separate, link, samples, TIFF_PLANARCONFIG_SEPARATE, 2); + + Mat decoded = imdecode(separate, IMREAD_ANYDEPTH | IMREAD_GRAYSCALE); + ASSERT_EQ(CV_16UC1, decoded.type()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), expected, decoded); +} + +TEST(Imgcodecs_Tiff, decode_planar_separate_packed) +{ + const int packed_bpps[] = { 10, 12, 14 }; + for (int i = 0; i < 3; i++) + { + const int bpp = packed_bpps[i]; + SCOPED_TRACE(cv::format("bpp=%d", bpp)); + const Mat truth = makePlanarTestMat(CV_16UC3, Size(21, 13)) & + Scalar::all((1 << bpp) - 1); + // The decoder scales packed samples up to 16 bits. + const Mat expected = truth * (1 << (16 - bpp)); + + std::vector contig, separate; + size_t link = 0; + appendTiffPage(contig, link, truth, TIFF_PLANARCONFIG_CONTIG, 2, Size(), bpp); + link = 0; + appendTiffPage(separate, link, truth, TIFF_PLANARCONFIG_SEPARATE, 2, Size(), bpp); + + Mat decodedContig = imdecode(contig, IMREAD_UNCHANGED); + ASSERT_PRED_FORMAT2(cvtest::MatComparator(0, 0), expected, decodedContig); + + Mat decodedSeparate = imdecode(separate, IMREAD_UNCHANGED); + ASSERT_FALSE(decodedSeparate.empty()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), expected, decodedSeparate); + } + + // tiled variant, width is a whole number of tiles so packed rows stay byte-aligned + const Mat truth = makePlanarTestMat(CV_16UC3, Size(32, 13)) & Scalar::all(0x0fff); + const Mat expected = truth * 16; + + std::vector contig, separate; + size_t link = 0; + appendTiffPage(contig, link, truth, TIFF_PLANARCONFIG_CONTIG, 0, Size(16, 16), 12); + link = 0; + appendTiffPage(separate, link, truth, TIFF_PLANARCONFIG_SEPARATE, 0, Size(16, 16), 12); + + Mat decodedContig = imdecode(contig, IMREAD_UNCHANGED); + ASSERT_PRED_FORMAT2(cvtest::MatComparator(0, 0), expected, decodedContig); + + Mat decodedSeparate = imdecode(separate, IMREAD_UNCHANGED); + ASSERT_FALSE(decodedSeparate.empty()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), expected, decodedSeparate); +} + +TEST(Imgcodecs_Tiff, decode_planar_separate_multipage) +{ + const Mat page0 = makePlanarTestMat(CV_16UC3, Size(21, 13)); + Mat page1; + bitwise_not(page0, page1); + + std::vector file; + size_t link = 0; + appendTiffPage(file, link, page0, TIFF_PLANARCONFIG_SEPARATE, 1); + appendTiffPage(file, link, page1, TIFF_PLANARCONFIG_SEPARATE, 3); + + std::vector pages; + ASSERT_TRUE(imdecodemulti(file, IMREAD_UNCHANGED, pages)); + ASSERT_EQ((size_t)2, pages.size()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), page0, pages[0]); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), page1, pages[1]); +} + #endif }} // namespace