/* * verify_seq_tiff.c — Read seq-* test TIFFs with libtiff and verify decoded values. * * This is the authoritative cross-check: libtiff wrote these files, and now * libtiff reads them back. The checksums computed here must match what * image-tiff's Rust decoder produces (in decode_images.rs tests). * * For sub-byte depths (1-7 bit): sums packed bytes (matching image-tiff behavior). * For extended depths (9-31 bit): unpacks MSB-first to u16/u32, sums those. * For standard depths: sums native sample values. * For float: sums as float/double. * For signed: sums as int64. * * Usage: ./verify_seq_tiff * Compile: cc -O2 -o verify_seq_tiff verify_seq_tiff.c -ltiff -lm */ #include #include #include #include #include #include #define W 16 #define H 16 /* Extract N bits from MSB-first packed byte stream starting at bit_offset. */ static uint32_t extract_bits_msb(const uint8_t *buf, int bit_offset, int nbits) { uint32_t val = 0; for (int i = nbits - 1; i >= 0; i--) { int byte_idx = bit_offset / 8; int bit_in_byte = bit_offset % 8; if (buf[byte_idx] & (0x80 >> bit_in_byte)) val |= (1u << i); bit_offset++; } return val; } typedef enum { SUM_U8_PACKED, /* Sum raw packed bytes (sub-byte depths) */ SUM_U8, /* Sum u8 samples */ SUM_U16, /* Sum u16 samples */ SUM_U16_UNPACK, /* Unpack from packed bits, sum as u16 */ SUM_U32, /* Sum u32 samples */ SUM_U32_UNPACK, /* Unpack from packed bits, sum as u32 */ SUM_U64, /* Sum u64 samples */ SUM_I8, /* Sum i8 samples */ SUM_I16, /* Sum i16 samples */ SUM_I32, /* Sum i32 samples */ SUM_F32, /* Sum f32 samples */ SUM_F64, /* Sum f64 samples */ } SumMode; typedef struct { const char *filename; SumMode mode; int bits_per_sample; int samples_per_pixel; /* Expected checksums — same as Rust test expectations */ union { uint64_t u; int64_t i; double f; } expected; } TestCase; static int verify_stripped(const char *dir, const TestCase *tc) { char path[512]; snprintf(path, sizeof(path), "%s/%s", dir, tc->filename); TIFF *tif = TIFFOpen(path, "r"); if (!tif) { fprintf(stderr, " SKIP %s (cannot open)\n", tc->filename); return 0; /* skip, not fail */ } uint32_t w, h; uint16_t bps, spp, sf, pc; TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w); TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h); TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bps); TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &spp); if (!TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &sf)) sf = SAMPLEFORMAT_UINT; if (!TIFFGetField(tif, TIFFTAG_PLANARCONFIG, &pc)) pc = PLANARCONFIG_CONTIG; int is_tiled = TIFFIsTiled(tif); tsize_t scanline_size = TIFFScanlineSize(tif); /* For planar images, read_image() only reads plane 0 */ int nplanes = (pc == PLANARCONFIG_SEPARATE) ? 1 : 1; /* spp for checksum is always actual spp for contig, 1 for planar (first plane) */ int eff_spp = (pc == PLANARCONFIG_SEPARATE) ? 1 : spp; uint64_t sum_u = 0; int64_t sum_i = 0; double sum_f = 0.0; if (is_tiled) { uint32_t tw, th; TIFFGetField(tif, TIFFTAG_TILEWIDTH, &tw); TIFFGetField(tif, TIFFTAG_TILELENGTH, &th); tsize_t tile_size = TIFFTileSize(tif); uint8_t *tile_buf = (uint8_t *)_TIFFmalloc(tile_size); if (!tile_buf) { TIFFClose(tif); return -1; } /* Assemble tiles into a full image buffer, then checksum */ int img_bytes; switch (tc->mode) { case SUM_U8_PACKED: case SUM_U8: img_bytes = w * h * eff_spp; break; case SUM_U16: case SUM_U16_UNPACK: img_bytes = w * h * eff_spp * 2; break; case SUM_U32: case SUM_U32_UNPACK: case SUM_F32: img_bytes = w * h * eff_spp * 4; break; case SUM_U64: case SUM_F64: img_bytes = w * h * eff_spp * 8; break; case SUM_I8: img_bytes = w * h * eff_spp; break; case SUM_I16: img_bytes = w * h * eff_spp * 2; break; case SUM_I32: img_bytes = w * h * eff_spp * 4; break; default: img_bytes = w * h * eff_spp; break; } uint8_t *img = (uint8_t *)calloc(1, img_bytes); if (!img) { _TIFFfree(tile_buf); TIFFClose(tif); return -1; } int bytes_per_sample = bps / 8; if (bytes_per_sample == 0) bytes_per_sample = 1; int row_bytes = w * eff_spp * bytes_per_sample; for (uint32_t ty = 0; ty < h; ty += th) { for (uint32_t tx = 0; tx < w; tx += tw) { TIFFReadTile(tif, tile_buf, tx, ty, 0, 0); /* Copy tile data into image buffer */ for (uint32_t row = 0; row < th && (ty + row) < h; row++) { int src_off = row * tw * eff_spp * bytes_per_sample; int dst_off = ((ty + row) * w + tx) * eff_spp * bytes_per_sample; int copy_w = tw; if (tx + tw > w) copy_w = w - tx; memcpy(img + dst_off, tile_buf + src_off, copy_w * eff_spp * bytes_per_sample); } } } /* Now checksum the assembled image */ int npixels = w * h * eff_spp; switch (tc->mode) { case SUM_U8_PACKED: case SUM_U8: for (int j = 0; j < npixels; j++) sum_u += img[j]; break; case SUM_U16: for (int j = 0; j < npixels; j++) sum_u += ((uint16_t *)img)[j]; break; case SUM_U32: for (int j = 0; j < npixels; j++) sum_u += ((uint32_t *)img)[j]; break; case SUM_I8: for (int j = 0; j < npixels; j++) sum_i += ((int8_t *)img)[j]; break; case SUM_F32: for (int j = 0; j < npixels; j++) sum_f += ((float *)img)[j]; break; case SUM_F64: for (int j = 0; j < npixels; j++) sum_f += ((double *)img)[j]; break; default: break; } free(img); _TIFFfree(tile_buf); } else { /* Stripped image */ uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); if (!buf) { TIFFClose(tif); return -1; } for (uint32_t y = 0; y < h; y++) { if (TIFFReadScanline(tif, buf, y, 0) < 0) { fprintf(stderr, " ERROR reading scanline %u of %s\n", y, tc->filename); _TIFFfree(buf); TIFFClose(tif); return -1; } switch (tc->mode) { case SUM_U8_PACKED: /* Sum raw packed bytes — matches image-tiff sub-byte output */ for (tsize_t j = 0; j < scanline_size; j++) sum_u += buf[j]; break; case SUM_U8: for (uint32_t x = 0; x < w * eff_spp; x++) sum_u += buf[x]; break; case SUM_U16: for (uint32_t x = 0; x < w * eff_spp; x++) sum_u += ((uint16_t *)buf)[x]; break; case SUM_U16_UNPACK: { /* Unpack N-bit MSB-first packed samples to u16 */ for (uint32_t x = 0; x < w * eff_spp; x++) { uint32_t val = extract_bits_msb(buf, x * bps, bps); sum_u += val; } break; } case SUM_U32: for (uint32_t x = 0; x < w * eff_spp; x++) sum_u += ((uint32_t *)buf)[x]; break; case SUM_U32_UNPACK: { for (uint32_t x = 0; x < w * eff_spp; x++) { uint32_t val = extract_bits_msb(buf, x * bps, bps); sum_u += val; } break; } case SUM_U64: for (uint32_t x = 0; x < w * eff_spp; x++) sum_u += ((uint64_t *)buf)[x]; break; case SUM_I8: for (uint32_t x = 0; x < w * eff_spp; x++) sum_i += ((int8_t *)buf)[x]; break; case SUM_I16: for (uint32_t x = 0; x < w * eff_spp; x++) sum_i += ((int16_t *)buf)[x]; break; case SUM_I32: for (uint32_t x = 0; x < w * eff_spp; x++) sum_i += ((int32_t *)buf)[x]; break; case SUM_F32: { float *fp = (float *)buf; for (uint32_t x = 0; x < w * eff_spp; x++) sum_f += fp[x]; break; } case SUM_F64: { double *dp = (double *)buf; for (uint32_t x = 0; x < w * eff_spp; x++) sum_f += dp[x]; break; } } } _TIFFfree(buf); } TIFFClose(tif); /* Compare checksums */ int pass = 0; char got_str[64], exp_str[64]; switch (tc->mode) { case SUM_U8_PACKED: case SUM_U8: case SUM_U16: case SUM_U16_UNPACK: case SUM_U32: case SUM_U32_UNPACK: case SUM_U64: snprintf(got_str, sizeof(got_str), "%llu", (unsigned long long)sum_u); snprintf(exp_str, sizeof(exp_str), "%llu", (unsigned long long)tc->expected.u); pass = (sum_u == tc->expected.u); break; case SUM_I8: case SUM_I16: case SUM_I32: snprintf(got_str, sizeof(got_str), "%lld", (long long)sum_i); snprintf(exp_str, sizeof(exp_str), "%lld", (long long)tc->expected.i); pass = (sum_i == tc->expected.i); break; case SUM_F32: snprintf(got_str, sizeof(got_str), "%.10g", sum_f); snprintf(exp_str, sizeof(exp_str), "%.10g", tc->expected.f); /* Allow small float rounding tolerance */ pass = (fabs(sum_f - tc->expected.f) < 0.01); break; case SUM_F64: snprintf(got_str, sizeof(got_str), "%.15g", sum_f); snprintf(exp_str, sizeof(exp_str), "%.15g", tc->expected.f); pass = (fabs(sum_f - tc->expected.f) < 1e-10); break; } if (pass) { printf(" PASS %-45s sum=%s\n", tc->filename, got_str); } else { printf(" FAIL %-45s got=%s expected=%s\n", tc->filename, got_str, exp_str); } return pass ? 0 : 1; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } const char *dir = argv[1]; /* These expected values MUST match the Rust test expectations in decode_images.rs. * If they don't, either the libtiff decoder or the image-tiff decoder has a bug. */ TestCase cases[] = { /* --- Sub-byte BlackIsZero (packed byte sums) --- */ {"seq-1c-3b.tiff", SUM_U8_PACKED, 3, 1, {.u = 5792}}, {"seq-1c-5b.tiff", SUM_U8_PACKED, 5, 1, {.u = 18944}}, {"seq-1c-7b.tiff", SUM_U8_PACKED, 7, 1, {.u = 24992}}, /* --- Sub-byte WhiteIsZero (packed byte sums) --- * libtiff returns RAW stored values (inverted), unlike image-tiff which * inverts back to BlackIsZero. These sums verify libtiff round-trips * the stored data correctly. image-tiff's inversion is verified separately * by the Rust tests (which expect the same sums as BlackIsZero). */ {"seq-1c-3b-miniswhite.tiff", SUM_U8_PACKED, 3, 1, {.u = 18688}}, {"seq-1c-5b-miniswhite.tiff", SUM_U8_PACKED, 5, 1, {.u = 21856}}, {"seq-1c-6b-miniswhite.tiff", SUM_U8_PACKED, 6, 1, {.u = 23232}}, {"seq-1c-7b-miniswhite.tiff", SUM_U8_PACKED, 7, 1, {.u = 32128}}, /* --- Extended BlackIsZero (unpacked u16/u32 sums) --- */ {"seq-1c-10b.tiff", SUM_U16_UNPACK, 10, 1, {.u = 32640}}, {"seq-1c-12b.tiff", SUM_U16_UNPACK, 12, 1, {.u = 32640}}, {"seq-1c-14b.tiff", SUM_U16_UNPACK, 14, 1, {.u = 32640}}, {"seq-1c-24b.tiff", SUM_U32_UNPACK, 24, 1, {.u = 32640}}, /* --- Extended WhiteIsZero (unpacked sums, decoder inverts) --- */ /* NOTE: libtiff does NOT invert WhiteIsZero — it returns raw stored values. * image-tiff inverts them. So for WIZ, libtiff sum ≠ image-tiff sum. * We verify these separately by checking the raw packed sum. */ /* --- Extended HPredict --- */ {"seq-1c-10b-hpredict.tiff", SUM_U16_UNPACK, 10, 1, {.u = 32640}}, {"seq-1c-12b-hpredict.tiff", SUM_U16_UNPACK, 12, 1, {.u = 32640}}, {"seq-1c-14b-hpredict.tiff", SUM_U16_UNPACK, 14, 1, {.u = 32640}}, {"seq-1c-24b-hpredict.tiff", SUM_U32_UNPACK, 24, 1, {.u = 32640}}, /* --- Extended RGB Contiguous (unpacked sums) --- */ {"seq-3c-10b-contig.tiff", SUM_U16_UNPACK, 10, 3, {.u = 294528}}, {"seq-3c-12b-contig.tiff", SUM_U16_UNPACK, 12, 3, {.u = 294528}}, {"seq-3c-14b-contig.tiff", SUM_U16_UNPACK, 14, 3, {.u = 294528}}, {"seq-3c-24b-contig.tiff", SUM_U32_UNPACK, 24, 3, {.u = 294528}}, /* --- Extended RGB Planar (first plane only, unpacked) --- */ {"seq-3c-10b-planar.tiff", SUM_U16_UNPACK, 10, 3, {.u = 32640}}, {"seq-3c-12b-planar.tiff", SUM_U16_UNPACK, 12, 3, {.u = 32640}}, {"seq-3c-14b-planar.tiff", SUM_U16_UNPACK, 14, 3, {.u = 32640}}, {"seq-3c-24b-planar.tiff", SUM_U32_UNPACK, 24, 3, {.u = 32640}}, /* --- Standard depths with compression --- */ {"seq-1c-8b-lzw.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-8b-deflate.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-8b-packbits.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-16b-lzw.tiff", SUM_U16, 16, 1, {.u = 32640}}, {"seq-1c-16b-deflate.tiff", SUM_U16, 16, 1, {.u = 32640}}, {"seq-1c-8b-lzw-hpredict.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-3c-8b-lzw.tiff", SUM_U8, 8, 3, {.u = 97920}}, /* --- Tiled --- */ {"seq-1c-8b-tiled.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-16b-tiled.tiff", SUM_U16, 16, 1, {.u = 32640}}, {"seq-3c-8b-tiled.tiff", SUM_U8, 8, 3, {.u = 97920}}, /* --- BigTIFF --- */ {"seq-1c-8b-bigtiff.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-3c-16b-bigtiff.tiff", SUM_U16, 16, 3, {.u = 294528}}, /* --- Float --- */ {"seq-1c-32f.tiff", SUM_F32, 32, 1, {.f = 128.0}}, {"seq-1c-64f.tiff", SUM_F64, 64, 1, {.f = 128.0}}, {"seq-3c-32f.tiff", SUM_F32, 32, 3, {.f = 384.0}}, /* --- Signed integer --- */ {"seq-1c-i8.tiff", SUM_I8, 8, 1, {.i = -128}}, {"seq-1c-i16.tiff", SUM_I16, 16, 1, {.i = -128}}, {"seq-1c-i32.tiff", SUM_I32, 32, 1, {.i = -128}}, /* --- CMYK --- */ {"seq-4c-8b-cmyk.tiff", SUM_U8, 8, 4, {.u = 130560}}, {"seq-4c-16b-cmyk.tiff", SUM_U16, 16, 4, {.u = 523776}}, /* --- Palette (raw packed indices) --- */ {"seq-1c-8b-palette.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-4b-palette.tiff", SUM_U8_PACKED, 4, 1, {.u = 15360}}, /* --- RGBA --- */ {"seq-4c-8b-rgba.tiff", SUM_U8, 8, 4, {.u = 130560}}, {"seq-4c-16b-rgba.tiff", SUM_U16, 16, 4, {.u = 523776}}, /* --- Multi-page (first page only) --- */ {"seq-1c-8b-multipage.tiff", SUM_U8, 8, 1, {.u = 32640}}, /* --- Big-endian --- */ {"seq-1c-8b-bigendian.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-16b-bigendian.tiff", SUM_U16, 16, 1, {.u = 32640}}, {"seq-3c-8b-bigendian.tiff", SUM_U8, 8, 3, {.u = 97920}}, /* --- Standard sub-byte (packed byte sums) --- */ {"seq-1c-1b.tiff", SUM_U8_PACKED, 1, 1, {.u = 2720}}, {"seq-1c-2b.tiff", SUM_U8_PACKED, 2, 1, {.u = 1728}}, {"seq-1c-4b.tiff", SUM_U8_PACKED, 4, 1, {.u = 15360}}, /* --- Multi-strip --- */ {"seq-1c-8b-multistrip.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-16b-multistrip.tiff", SUM_U16, 16, 1, {.u = 32640}}, {"seq-3c-8b-multistrip.tiff", SUM_U8, 8, 3, {.u = 97920}}, /* --- Tiled + compressed --- */ {"seq-1c-8b-tiled-lzw.tiff", SUM_U8, 8, 1, {.u = 32640}}, {"seq-1c-8b-tiled-deflate.tiff", SUM_U8, 8, 1, {.u = 32640}}, /* --- Float + predictor --- */ {"seq-1c-32f-deflate-fpredict.tiff", SUM_F32, 32, 1, {.f = 128.0}}, {"seq-1c-64f-deflate-fpredict.tiff", SUM_F64, 64, 1, {.f = 128.0}}, /* --- Unassociated alpha --- */ {"seq-4c-8b-rgba-unassoc.tiff", SUM_U8, 8, 4, {.u = 130560}}, /* --- Sub-byte RGB (packed byte sums) --- */ {"seq-3c-5b-contig.tiff", SUM_U8_PACKED, 5, 3, {.u = 56832}}, {"seq-3c-7b-contig.tiff", SUM_U8_PACKED, 7, 3, {.u = 74976}}, /* --- Signed RGB --- */ {"seq-3c-i8.tiff", SUM_I8, 8, 3, {.i = -384}}, {"seq-3c-i16.tiff", SUM_I16, 16, 3, {.i = -384}}, /* --- Float f64 RGB --- */ {"seq-3c-64f.tiff", SUM_F64, 64, 3, {.f = 384.0}}, /* --- Tiled BigTIFF --- */ {"seq-1c-8b-tiled-bigtiff.tiff", SUM_U8, 8, 1, {.u = 32640}}, /* --- CCITT Group 3 (Fax3) --- * libtiff returns raw scanline data in MinIsWhite convention (0=white, 1=black). * The stored pattern is inverted sequential, so libtiff sum differs from * image-tiff's BlackIsZero sum. For 16x16 1-bit with inverted sequential pattern, * the raw packed byte sum is computed by verify run below. */ {"seq-1c-1b-fax3.tiff", SUM_U8_PACKED, 1, 1, {.u = 5440}}, {"seq-1c-1b-fax3-fillbits.tiff", SUM_U8_PACKED, 1, 1, {.u = 5440}}, {"seq-1c-1b-fax3-lsb.tiff", SUM_U8_PACKED, 1, 1, {.u = 5440}}, }; int ncases = sizeof(cases) / sizeof(cases[0]); int failures = 0; printf("Verifying %d test images with libtiff decoder in %s/\n\n", ncases, dir); /* Suppress libtiff warnings for non-standard tag 317 (Predictor) */ TIFFSetWarningHandler(NULL); for (int i = 0; i < ncases; i++) { int ret = verify_stripped(dir, &cases[i]); if (ret != 0) failures++; } printf("\n%d/%d passed", ncases - failures, ncases); if (failures > 0) printf(", %d FAILED", failures); printf("\n"); return failures > 0 ? 1 : 0; }