PR #22895 opened by sohamukute
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22895
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22895.patch

Adds a synthetic TIFF decoder FATE test that exercises decoder pathsnot 
reachable via encoder roundtrip, including: big-endian byte order,photometric 
inversion, PAL8, FillOrder, GRAY12, RATIONAL/ASCII/GeoTIFF
metadata, DNG, PackBits, ADOBE_DEFLATE, horizontal predictor, 
RGB24,GRAY_RESPONSE_CURVE, RGB48LE, 16-bit CMYK (RGBA64BE), and DNG BayerRGGB8 
with color matrix processing.



From eb1f78e1c8b9a329d0a0dcf0ee8bc5aca4357db4 Mon Sep 17 00:00:00 2001
From: Soham Kute <[email protected]>
Date: Thu, 23 Apr 2026 01:48:39 +0530
Subject: [PATCH] avcodec/tests/tiffdec: add synthetic TIFF decoder FATE test

Adds a synthetic TIFF decoder test program that exercises decoder paths
not reachable via encoder roundtrip, including: big-endian (MM) byte
order, GRAY16BE, WHITE_IS_ZERO photometric inversion, PAL8 with ColorMap,
FillOrder=2 byte reversal, GRAY12 packed pixels, RATIONAL metadata tags,
ASCII metadata, GeoTIFF tags, DNG decode path, PackBits and ADOBE_DEFLATE
compression, horizontal predictor (GRAY8 and GRAY16LE), RGB24,
GRAY_RESPONSE_CURVE (dng_lut), RGB48LE (init_image case 483), 16-bit CMYK
in big-endian (init_image case 644 + RGBA64BE conversion path), and a
DNG Bayer RGGB8 image with DNG_COLOR_MATRIX1 and DNG_CAMERA_CALIBRATION1
tags exercising camera_xyz_coeff() and the color-matrix else-branch.
---
 libavcodec/Makefile        |    1 +
 libavcodec/tests/tiffdec.c | 1162 ++++++++++++++++++++++++++++++++++++
 tests/fate/libavcodec.mak  |    4 +
 tests/ref/fate/tiffdec     |   21 +
 4 files changed, 1188 insertions(+)
 create mode 100644 libavcodec/tests/tiffdec.c
 create mode 100644 tests/ref/fate/tiffdec

diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 687121e337..1c5d3c5b48 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -1384,6 +1384,7 @@ TESTPROGS-$(CONFIG_H264_METADATA_BSF)     += h264_levels
 TESTPROGS-$(CONFIG_HEVC_METADATA_BSF)     += h265_levels
 TESTPROGS-$(CONFIG_RANGECODER)            += rangecoder
 TESTPROGS-$(CONFIG_SNOW_ENCODER)          += snowenc
+TESTPROGS-$(CONFIG_TIFF_DECODER)          += tiffdec
 
 TESTOBJS = dctref.o
 
diff --git a/libavcodec/tests/tiffdec.c b/libavcodec/tests/tiffdec.c
new file mode 100644
index 0000000000..acac4ae0a0
--- /dev/null
+++ b/libavcodec/tests/tiffdec.c
@@ -0,0 +1,1162 @@
+/*
+ * Synthetic TIFF decoder tests - exercises decoder paths not
+ * reachable via encoder roundtrip: big-endian (MM) byte order,
+ * GRAY16BE pixel format, WHITE_IS_ZERO photometric inversion,
+ * palette-indexed (PAL8) images with a ColorMap tag, FillOrder=2
+ * byte reversal, GRAY12 packed pixels, RATIONAL metadata tags
+ * (XResolution/YResolution for SAR), ASCII metadata (Artist/Software/
+ * Date), GeoTIFF tags (ModelPixelScale, ModelTiepoint,
+ * GeoKeyDirectory, GeoDoubleParams, GeoAsciiParams), DNG_VERSION tag
+ * triggering the DNG decode path (dng_process_color8, dng_blit),
+ * PackBits compression (literal and repeat runs), and
+ * ADOBE_DEFLATE/zlib compression.
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdint.h>
+#include <string.h>
+
+#include "libavcodec/avcodec.h"
+#include "libavutil/mem.h"
+#include "libavutil/pixdesc.h"
+
+/* Little-endian byte writers */
+static void w16le(uint8_t *b, int *p, uint16_t v)
+{
+    b[(*p)++] =  v        & 0xFF;
+    b[(*p)++] = (v >>  8) & 0xFF;
+}
+
+static void w32le(uint8_t *b, int *p, uint32_t v)
+{
+    b[(*p)++] =  v        & 0xFF;
+    b[(*p)++] = (v >>  8) & 0xFF;
+    b[(*p)++] = (v >> 16) & 0xFF;
+    b[(*p)++] = (v >> 24) & 0xFF;
+}
+
+/* Write an IEEE 754 double as 8 bytes, little-endian. */
+static void w64le(uint8_t *b, int *p, double v)
+{
+    union { double d; uint64_t u; } cv;
+    cv.d = v;
+    w32le(b, p, (uint32_t)(cv.u & 0xFFFFFFFF));
+    w32le(b, p, (uint32_t)(cv.u >> 32));
+}
+
+/* Big-endian byte writers */
+static void w16be(uint8_t *b, int *p, uint16_t v)
+{
+    b[(*p)++] = (v >>  8) & 0xFF;
+    b[(*p)++] =  v        & 0xFF;
+}
+
+static void w32be(uint8_t *b, int *p, uint32_t v)
+{
+    b[(*p)++] = (v >> 24) & 0xFF;
+    b[(*p)++] = (v >> 16) & 0xFF;
+    b[(*p)++] = (v >>  8) & 0xFF;
+    b[(*p)++] =  v        & 0xFF;
+}
+
+/*
+ * Write a 12-byte IFD entry (little-endian).
+ * SHORT (type 3) count-1 values are left-justified in the 4-byte
+ * value field per the TIFF spec; all other cases write 4 bytes.
+ */
+static void ifd_le(uint8_t *b, int *p, uint16_t tag, uint16_t type,
+                   uint32_t count, uint32_t value)
+{
+    w16le(b, p, tag);
+    w16le(b, p, type);
+    w32le(b, p, count);
+    if (type == 3 && count == 1) {
+        w16le(b, p, (uint16_t)value);
+        w16le(b, p, 0);
+    } else {
+        w32le(b, p, value);
+    }
+}
+
+/*
+ * Write a 12-byte IFD entry (big-endian).
+ * Same left-justification rule applies.
+ */
+static void ifd_be(uint8_t *b, int *p, uint16_t tag, uint16_t type,
+                   uint32_t count, uint32_t value)
+{
+    w16be(b, p, tag);
+    w16be(b, p, type);
+    w32be(b, p, count);
+    if (type == 3 && count == 1) {
+        w16be(b, p, (uint16_t)value);
+        w16be(b, p, 0);
+    } else {
+        w32be(b, p, value);
+    }
+}
+
+/*
+ * Open the TIFF decoder, send one synthetic packet, and return the
+ * decoded AVPixelFormat on success or a negative error code.
+ */
+static int decode_tiff(const uint8_t *data, int size)
+{
+    const AVCodec *codec;
+    AVCodecContext *avctx;
+    AVPacket *pkt;
+    AVFrame *frame;
+    int ret, fmt = -1;
+
+    codec = avcodec_find_decoder(AV_CODEC_ID_TIFF);
+    if (!codec)
+        return -1;
+
+    avctx = avcodec_alloc_context3(codec);
+    if (!avctx)
+        return -1;
+
+    if ((ret = avcodec_open2(avctx, codec, NULL)) < 0) {
+        avcodec_free_context(&avctx);
+        return ret;
+    }
+
+    pkt   = av_packet_alloc();
+    frame = av_frame_alloc();
+    if (!pkt || !frame) {
+        ret = -1;
+        goto out;
+    }
+
+    if ((ret = av_new_packet(pkt, size)) < 0)
+        goto out;
+
+    memcpy(pkt->data, data, size);
+    ret = avcodec_send_packet(avctx, pkt);
+    if (ret >= 0)
+        ret = avcodec_receive_frame(avctx, frame);
+    if (ret >= 0)
+        fmt = frame->format;
+
+out:
+    av_frame_free(&frame);
+    av_packet_free(&pkt);
+    avcodec_free_context(&avctx);
+    return ret >= 0 ? fmt : ret;
+}
+
+/*
+ * Big-endian (MM byte order) 2x2 GRAY8 image, RAW compression,
+ * BLACK_IS_ZERO photometric.  Exercises the s->le=0 path throughout
+ * tiff_decode_tag (all ff_tget calls use AV_RB16/AV_RB32).
+ *
+ * Layout: header(8) + IFD(2 + 8*12 + 4) + pixel_data(4) = 114 bytes
+ */
+static int test_be_gray8(void)
+{
+    uint8_t buf[114];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x4D; buf[p++] = 0x4D;  /* "MM" */
+    w16be(buf, &p, 42);
+    w32be(buf, &p, 8);                  /* IFD at offset 8 */
+
+    w16be(buf, &p, 8);                  /* 8 IFD entries */
+    ifd_be(buf, &p, 0x0100, 3, 1,   2);  /* ImageWidth */
+    ifd_be(buf, &p, 0x0101, 3, 1,   2);  /* ImageLength */
+    ifd_be(buf, &p, 0x0102, 3, 1,   8);  /* BitsPerSample */
+    ifd_be(buf, &p, 0x0103, 3, 1,   1);  /* Compression=RAW */
+    ifd_be(buf, &p, 0x0106, 3, 1,   1);  /* 
PhotometricInterpretation=BLACK_IS_ZERO */
+    ifd_be(buf, &p, 0x0111, 4, 1, 110);  /* StripOffsets */
+    ifd_be(buf, &p, 0x0116, 3, 1,   2);  /* RowsPerStrip */
+    ifd_be(buf, &p, 0x0117, 4, 1,   4);  /* StripByteCounts */
+    w32be(buf, &p, 0);                   /* next IFD = none */
+
+    buf[p++] = 0xFF; buf[p++] = 0x80;
+    buf[p++] = 0x40; buf[p++] = 0x00;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Big-endian (MM) 2x2 GRAY16 image, RAW compression.
+ * Exercises the AV_PIX_FMT_GRAY16BE pixel-format selection path
+ * (bpp=16, s->le=0 -> case 161 in init_image).
+ *
+ * Layout: header(8) + IFD(2 + 8*12 + 4) + pixel_data(8) = 118 bytes
+ */
+static int test_be_gray16(void)
+{
+    uint8_t buf[118];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x4D; buf[p++] = 0x4D;
+    w16be(buf, &p, 42);
+    w32be(buf, &p, 8);
+
+    w16be(buf, &p, 8);
+    ifd_be(buf, &p, 0x0100, 3, 1,   2);
+    ifd_be(buf, &p, 0x0101, 3, 1,   2);
+    ifd_be(buf, &p, 0x0102, 3, 1,  16);  /* BitsPerSample=16 */
+    ifd_be(buf, &p, 0x0103, 3, 1,   1);
+    ifd_be(buf, &p, 0x0106, 3, 1,   1);
+    ifd_be(buf, &p, 0x0111, 4, 1, 110);
+    ifd_be(buf, &p, 0x0116, 3, 1,   2);
+    ifd_be(buf, &p, 0x0117, 4, 1,   8);  /* StripByteCounts=8 */
+    w32be(buf, &p, 0);
+
+    w16be(buf, &p, 0xFFFF); w16be(buf, &p, 0x8000);
+    w16be(buf, &p, 0x4000); w16be(buf, &p, 0x0001);
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with WHITE_IS_ZERO (MinIsWhite) photometric.
+ * Exercises the sample-inversion loop near the end of decode_frame
+ * where each output byte becomes (255 - byte).
+ *
+ * Layout: header(8) + IFD(2 + 8*12 + 4) + pixel_data(4) = 114 bytes
+ */
+static int test_miniswhite(void)
+{
+    uint8_t buf[114];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;  /* "II" */
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 8);
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);
+    ifd_le(buf, &p, 0x0102, 3, 1,   8);
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);
+    ifd_le(buf, &p, 0x0106, 3, 1,   0);  /* 
PhotometricInterpretation=WHITE_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 4, 1, 110);
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);
+    ifd_le(buf, &p, 0x0117, 4, 1,   4);
+    w32le(buf, &p, 0);
+
+    buf[p++] = 0xFF; buf[p++] = 0x80;
+    buf[p++] = 0x40; buf[p++] = 0x00;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 PAL8 image with a ColorMap tag.
+ * Exercises the TIFF_PAL handler (palette reading, palette_is_set=1)
+ * and the AV_PIX_FMT_PAL8 pixel-format path in init_image.
+ *
+ * Layout:
+ *   header(8) + IFD(2 + 9*12 + 4) + colormap(768*2) + pixel_data(4)
+ *   = 8 + 114 + 1536 + 4 = 1662 bytes
+ *
+ * The ColorMap holds 256 R, 256 G, 256 B entries as uint16le.
+ * The decoder shifts each 16-bit value right by 8 bits, so entry i
+ * maps to color (i, i, i) with a grayscale ramp of i*257.
+ */
+static int test_pal8(void)
+{
+    uint8_t buf[1662];
+    int p = 0, i;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                         /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3,   1,    2);
+    ifd_le(buf, &p, 0x0101, 3,   1,    2);
+    ifd_le(buf, &p, 0x0102, 3,   1,    8);
+    ifd_le(buf, &p, 0x0103, 3,   1,    1);
+    ifd_le(buf, &p, 0x0106, 3,   1,    3);     /* 
PhotometricInterpretation=PALETTE */
+    ifd_le(buf, &p, 0x0111, 4,   1, 1658);     /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3,   1,    2);
+    ifd_le(buf, &p, 0x0117, 4,   1,    4);
+    ifd_le(buf, &p, 0x0140, 3, 768,  122);     /* ColorMap at offset 122 */
+    w32le(buf, &p, 0);
+
+    for (i = 0; i < 256; i++) w16le(buf, &p, i * 257);  /* R[0..255] */
+    for (i = 0; i < 256; i++) w16le(buf, &p, i * 257);  /* G[0..255] */
+    for (i = 0; i < 256; i++) w16le(buf, &p, i * 257);  /* B[0..255] */
+
+    buf[p++] = 0x00; buf[p++] = 0x80;
+    buf[p++] = 0xFF; buf[p++] = 0x40;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with FillOrder=2 (LSB-to-MSB bit order).
+ * Exercises the TIFF_FILL_ORDER tag handler (s->fill_order = value - 1)
+ * and the ff_reverse[src[i]] byte-reversal loop in tiff_unpack_strip
+ * (TIFF_RAW branch, fill_order != 0 path).
+ *
+ * Layout: header(8) + IFD(2 + 9*12 + 4) + pixel_data(4) = 126 bytes
+ */
+static int test_fill_order(void)
+{
+    uint8_t buf[126];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x010A, 3, 1,   2);         /* FillOrder=2 (LSB first) */
+    ifd_le(buf, &p, 0x0111, 4, 1, 122);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,   4);         /* StripByteCounts */
+    w32le(buf, &p, 0);
+
+    buf[p++] = 0xFF; buf[p++] = 0x80;
+    buf[p++] = 0x40; buf[p++] = 0x00;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY12 image, RAW compression.
+ * Exercises AV_PIX_FMT_GRAY12 pixel-format selection (bpp=12, case 121
+ * in init_image) and the unpack_gray() bitstream-reader path in
+ * tiff_unpack_strip.
+ *
+ * Each row is 3 bytes: 2 pixels x 12 bits = 24 bits packed MSB-first.
+ * Layout: header(8) + IFD(2 + 8*12 + 4) + pixel_data(6) = 116 bytes
+ */
+static int test_gray12(void)
+{
+    uint8_t buf[116];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 8);                          /* 8 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,  12);         /* BitsPerSample=12 */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 4, 1, 110);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,   6);         /* StripByteCounts=6 (3 
bytes/row x 2) */
+    w32le(buf, &p, 0);
+
+    /* 2 rows x 3 bytes: packed 12-bit samples, all bits set */
+    buf[p++] = 0xFF; buf[p++] = 0xFF; buf[p++] = 0xFF;
+    buf[p++] = 0xFF; buf[p++] = 0xFF; buf[p++] = 0xFF;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with RATIONAL XResolution/YResolution tags,
+ * a PageNumber tag (two packed SHORTs), and ASCII metadata tags for
+ * Artist, SoftwareName, and Date.
+ *
+ * Exercises: set_sar() (XResolution/YResolution RATIONAL path),
+ * the PageNumber SHORT-count-2 handler, and ff_tadd_string_metadata()
+ * via ADD_METADATA for all three string tags.
+ *
+ * Layout:
+ *   header(8) + IFD(2 + 14*12 + 4) + xres(8) + yres(8) +
+ *   sw(7) + date(20) + artist(5) + pixels(4) = 234 bytes
+ *
+ * Tag offsets after IFD end at 182:
+ *   182: XResolution  72/1
+ *   190: YResolution  72/1
+ *   198: SoftwareName "FFmpeg\0"
+ *   205: Date         "2024:01:01 00:00:00\0"
+ *   225: Artist       "Test\0"
+ *   230: pixel data
+ */
+static int test_metadata(void)
+{
+    static const char sw[]     = "FFmpeg";
+    static const char date[]   = "2024:01:01 00:00:00";
+    static const char artist[] = "Test";
+    uint8_t buf[234];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 14);                          /* 14 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3,  1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3,  1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3,  1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3,  1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3,  1,   1);         /* Photometric=BLACK_IS_ZERO 
*/
+    ifd_le(buf, &p, 0x0111, 4,  1, 230);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3,  1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4,  1,   4);         /* StripByteCounts */
+    ifd_le(buf, &p, 0x011A, 5,  1, 182);         /* XResolution: RATIONAL at 
182 */
+    ifd_le(buf, &p, 0x011B, 5,  1, 190);         /* YResolution: RATIONAL at 
190 */
+    ifd_le(buf, &p, 0x0129, 3,  2, 0u|(1u<<16)); /* PageNumber: page=0, 
total=1 */
+    ifd_le(buf, &p, 0x0131, 2,  7, 198);         /* SoftwareName: 7 bytes at 
198 */
+    ifd_le(buf, &p, 0x0132, 2, 20, 205);         /* Date: 20 bytes at 205 */
+    ifd_le(buf, &p, 0x013B, 2,  5, 225);         /* Artist: 5 bytes at 225 */
+    w32le(buf, &p, 0);
+
+    /* p == 182 here */
+    w32le(buf, &p, 72); w32le(buf, &p, 1);       /* XResolution 72/1 */
+    w32le(buf, &p, 72); w32le(buf, &p, 1);       /* YResolution 72/1 */
+    memcpy(buf + p, sw,     sizeof(sw));     p += sizeof(sw);      /* offset 
198 */
+    memcpy(buf + p, date,   sizeof(date));   p += sizeof(date);    /* offset 
205 */
+    memcpy(buf + p, artist, sizeof(artist)); p += sizeof(artist);  /* offset 
225 */
+
+    buf[p++] = 0xFF; buf[p++] = 0x80;            /* pixel data at offset 230 */
+    buf[p++] = 0x40; buf[p++] = 0x00;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 GeoTIFF with ModelPixelScale, ModelTiepoint,
+ * GeoKeyDirectory, GeoDoubleParams, and GeoAsciiParams tags.
+ *
+ * Exercises: tiff_set_type, free_geotags, get_geokey_name,
+ * get_geokey_type, get_geokey_val (GT_MODEL_TYPE and ANGULAR_UNITS via
+ * RET_GEOKEY_VAL, PROJECTED_CS_TYPE via search_keyval/cmp_id_key),
+ * doubles2str, add_metadata (DOUBLE and STRING paths).
+ *
+ * GeoKey directory has 5 keys:
+ *   1024 (GT_MODEL_TYPE)    inline val=1
+ *   2049 (GEOG_CITATION)    GeoAsciiParams offset=0, count=6
+ *   2054 (ANGULAR_UNITS)    inline val=9101
+ *   2057 (SEMI_MAJOR_AXIS)  GeoDoubleParams offset=0, count=1
+ *   3072 (PROJECTED_CS)     inline val=3 (exercises search_keyval)
+ *
+ * Layout:
+ *   header(8) + IFD(2 + 13*12 + 4)
+ *   + mps(24) + mtp(48) + gkd(48) + gdp(8) + gap(6) + pixels(4)
+ *   = 170 + 138 = 308 bytes
+ *
+ * Data offsets after IFD end at 170:
+ *   170: ModelPixelScale  3 doubles
+ *   194: ModelTiepoint    6 doubles
+ *   242: GeoKeyDirectory  24 shorts
+ *   290: GeoDoubleParams  1 double (6378137.0)
+ *   298: GeoAsciiParams   "WGS84|"
+ *   304: pixel data
+ */
+static int test_geotiff(void)
+{
+    uint8_t buf[308];
+    int p = 0, i;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 13);                           /* 13 IFD entries */
+    ifd_le(buf, &p, 0x0100,  3,  1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101,  3,  1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102,  3,  1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103,  3,  1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106,  3,  1,   1);         /* Photometric=BLACK_IS_ZERO 
*/
+    ifd_le(buf, &p, 0x0111,  4,  1, 304);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116,  3,  1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117,  4,  1,   4);         /* StripByteCounts */
+    ifd_le(buf, &p, 0x830E, 12,  3, 170);         /* ModelPixelScale: 3 
doubles */
+    ifd_le(buf, &p, 0x8482, 12,  6, 194);         /* ModelTiepoint: 6 doubles 
*/
+    ifd_le(buf, &p, 0x87AF,  3, 24, 242);         /* GeoKeyDirectory: 24 
shorts */
+    ifd_le(buf, &p, 0x87B0, 12,  1, 290);         /* GeoDoubleParams: 1 double 
*/
+    ifd_le(buf, &p, 0x87B1,  2,  6, 298);         /* GeoAsciiParams: 6 bytes */
+    w32le(buf, &p, 0);
+
+    /* p == 170 */
+    /* ModelPixelScale: scale_x=1.0, scale_y=1.0, scale_z=0.0 */
+    w64le(buf, &p, 1.0); w64le(buf, &p, 1.0); w64le(buf, &p, 0.0);
+
+    /* ModelTiepoint: 6 doubles (i,j,k, x,y,z) all zero */
+    for (i = 0; i < 6; i++) w64le(buf, &p, 0.0);
+
+    /* GeoKeyDirectory: header + 5 key entries = 24 shorts */
+    w16le(buf, &p, 1); w16le(buf, &p, 1); w16le(buf, &p, 0); w16le(buf, &p, 5);
+    w16le(buf, &p, 1024); w16le(buf, &p,     0); w16le(buf, &p, 1); w16le(buf, 
&p,    1);
+    w16le(buf, &p, 2049); w16le(buf, &p, 34737); w16le(buf, &p, 6); w16le(buf, 
&p,    0);
+    w16le(buf, &p, 2054); w16le(buf, &p,     0); w16le(buf, &p, 1); w16le(buf, 
&p, 9101);
+    w16le(buf, &p, 2057); w16le(buf, &p, 34736); w16le(buf, &p, 1); w16le(buf, 
&p,    0);
+    w16le(buf, &p, 3072); w16le(buf, &p,     0); w16le(buf, &p, 1); w16le(buf, 
&p,    3);
+
+    /* GeoDoubleParams: WGS84 semi-major axis */
+    w64le(buf, &p, 6378137.0);
+
+    /* GeoAsciiParams: delimiter-terminated string (last byte replaced with 
NUL) */
+    buf[p++] = 'W'; buf[p++] = 'G'; buf[p++] = 'S';
+    buf[p++] = '8'; buf[p++] = '4'; buf[p++] = '|';
+
+    buf[p++] = 0xFF; buf[p++] = 0x80;            /* pixel data at offset 304 */
+    buf[p++] = 0x40; buf[p++] = 0x00;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 YCbCr 4:2:0 image.
+ * Exercises: TIFF_YCBCR_SUBSAMPLING tag handler (s->subsampling[]),
+ * init_image case 243 YCbCr path (AV_PIX_FMT_YUV420P), the is_yuv
+ * branch in tiff_unpack_strip (yuv_line buffer, width recomputation),
+ * and unpack_yuv() which writes to the three output planes.
+ *
+ * Strip data (6 bytes): [Y00, Y01, Y10, Y11, Cb, Cr] packed YCbCr block.
+ *
+ * Layout:
+ *   header(8) + IFD(2+10*12+4) + bps_data(6) + strip(6) = 146 bytes
+ *
+ * BitsPerSample=[8,8,8] at offset 134; strip at offset 140.
+ */
+static int test_ycbcr(void)
+{
+    uint8_t buf[146];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 10);                         /* 10 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3,  1,   2);        /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3,  1,   2);        /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3,  3, 134);        /* BitsPerSample=[8,8,8] at 
134 */
+    ifd_le(buf, &p, 0x0103, 3,  1,   1);        /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3,  1,   6);        /* Photometric=YCBCR */
+    ifd_le(buf, &p, 0x0111, 4,  1, 140);        /* StripOffsets */
+    ifd_le(buf, &p, 0x0115, 3,  1,   3);        /* SamplesPerPixel=3 */
+    ifd_le(buf, &p, 0x0116, 3,  1,   2);        /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4,  1,   6);        /* StripByteCounts */
+    /* YCbCrSubSampling=[2,2] inline: two SHORTs in 4-byte field */
+    ifd_le(buf, &p, 0x0212, 3,  2, 0x00020002);
+    w32le(buf, &p, 0);
+
+    /* p == 134: BitsPerSample data */
+    w16le(buf, &p, 8); w16le(buf, &p, 8); w16le(buf, &p, 8);
+
+    /* p == 140: YCbCr strip: Y00, Y01, Y10, Y11, Cb, Cr */
+    buf[p++] = 0x80; buf[p++] = 0x80;
+    buf[p++] = 0x80; buf[p++] = 0x80;
+    buf[p++] = 0x80; buf[p++] = 0x80;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 CMYK image (SEPARATED photometric).
+ * Exercises: init_image case 324 (AV_PIX_FMT_RGB0 for SEPARATED),
+ * the CMYK-to-RGB conversion loop in decode_frame (lines 2360-2373),
+ * and multi-component BitsPerSample handler (bppcount>1 sum loop).
+ *
+ * Layout:
+ *   header(8) + IFD(2+9*12+4) + bps_data(8) + strip(16) = 146 bytes
+ *
+ * BitsPerSample=[8,8,8,8] at 122; strip at 130.
+ */
+static int test_cmyk(void)
+{
+    uint8_t buf[146];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 4, 122);         /* BitsPerSample=[8,8,8,8] at 
122 */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   5);         /* Photometric=SEPARATED 
(CMYK) */
+    ifd_le(buf, &p, 0x0111, 4, 1, 130);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0115, 3, 1,   4);         /* SamplesPerPixel=4 */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,  16);         /* StripByteCounts=16 */
+    w32le(buf, &p, 0);
+
+    /* p == 122: BitsPerSample = [8, 8, 8, 8] */
+    w16le(buf, &p, 8); w16le(buf, &p, 8);
+    w16le(buf, &p, 8); w16le(buf, &p, 8);
+
+    /* p == 130: CMYK strip (4 pixels x 4 bytes each = 16 bytes) */
+    /* Row 0: pixel0 C=0,M=0,Y=0,K=0 (white); pixel1 C=0,M=0,Y=0,K=255 (black) 
*/
+    buf[p++]=0;   buf[p++]=0;   buf[p++]=0;   buf[p++]=0;
+    buf[p++]=0;   buf[p++]=0;   buf[p++]=0;   buf[p++]=255;
+    /* Row 1: pixel2 C=255,M=0,Y=0,K=0; pixel3 C=0,M=255,Y=0,K=0 */
+    buf[p++]=255; buf[p++]=0;   buf[p++]=0;   buf[p++]=0;
+    buf[p++]=0;   buf[p++]=255; buf[p++]=0;   buf[p++]=0;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with additional ASCII metadata tags:
+ * DocumentName (0x010D), ImageDescription (0x010E), Make (0x010F),
+ * Model (0x0110), PageName (0x011D), HostComputer (0x013C).
+ * Exercises: ADD_METADATA for all six handlers in tiff_decode_tag
+ * (lines 1747-1764).  Each string has count=5 (>4) so ff_tread_tag
+ * seeks to the off-IFD string data.
+ *
+ * Layout:
+ *   header(8) + IFD(2+14*12+4) + 6 strings*5b + pixels(4) = 216 bytes
+ *
+ * Strings at 182: "doc1\0","img1\0","cam1\0","mdl1\0","pg1a\0","hst1\0"
+ */
+static int test_more_metadata(void)
+{
+    static const char doc[]  = "doc1";
+    static const char img[]  = "img1";
+    static const char cam[]  = "cam1";
+    static const char mdl[]  = "mdl1";
+    static const char pg[]   = "pg1a";
+    static const char hst[]  = "hst1";
+    uint8_t buf[216];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 14);                          /* 14 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3,  1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3,  1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3,  1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3,  1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3,  1,   1);         /* Photometric=BLACK_IS_ZERO 
*/
+    ifd_le(buf, &p, 0x010D, 2,  5, 182);         /* DocumentName at 182 */
+    ifd_le(buf, &p, 0x010E, 2,  5, 187);         /* ImageDescription at 187 */
+    ifd_le(buf, &p, 0x010F, 2,  5, 192);         /* Make at 192 */
+    ifd_le(buf, &p, 0x0110, 2,  5, 197);         /* Model at 197 */
+    ifd_le(buf, &p, 0x0111, 4,  1, 212);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3,  1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4,  1,   4);         /* StripByteCounts */
+    ifd_le(buf, &p, 0x011D, 2,  5, 202);         /* PageName at 202 */
+    ifd_le(buf, &p, 0x013C, 2,  5, 207);         /* HostComputer at 207 */
+    w32le(buf, &p, 0);
+
+    /* p == 182: string data */
+    memcpy(buf + p, doc, 5); p += 5;
+    memcpy(buf + p, img, 5); p += 5;
+    memcpy(buf + p, cam, 5); p += 5;
+    memcpy(buf + p, mdl, 5); p += 5;
+    memcpy(buf + p, pg,  5); p += 5;
+    memcpy(buf + p, hst, 5); p += 5;
+
+    /* p == 212: pixel data */
+    buf[p++] = 0xFF; buf[p++] = 0x80;
+    buf[p++] = 0x40; buf[p++] = 0x00;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY16LE with Predictor=2 (horizontal differencing).
+ * Exercises: TIFF_PREDICTOR tag handler (s->predictor=2, lines 1452-1456),
+ * and the 16-bit LE predictor loop in decode_frame (lines 2248-2258).
+ *
+ * Each row stores pixel differences: actual[j] = stored[j] + actual[j-1].
+ * Strip [0x01,0x00, 0x01,0x00, ...] decodes to [1, 2] per row.
+ *
+ * Layout: header(8) + IFD(2+9*12+4) + strip(8) = 130 bytes
+ */
+static int test_predictor_gray16le(void)
+{
+    uint8_t buf[130];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,  16);         /* BitsPerSample=16 */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 4, 1, 122);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,   8);         /* StripByteCounts=8 */
+    ifd_le(buf, &p, 0x013D, 3, 1,   2);         /* Predictor=2 */
+    w32le(buf, &p, 0);
+
+    /* 2 rows x 2 pixels x 2 bytes: each pixel stores the delta from previous 
*/
+    w16le(buf, &p, 0x0001); w16le(buf, &p, 0x0001); /* row 0: [1, 1+1=2] */
+    w16le(buf, &p, 0x0001); w16le(buf, &p, 0x0001); /* row 1: [1, 2] */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with Predictor=2 (horizontal differencing).
+ * Exercises the 8-bit (else) branch of the predictor post-processing
+ * loop in decode_frame (lines 2270-2278).
+ *
+ * Row data stores pixel differences; decoding adds each delta to previous.
+ * Strip [0x80,0x10, 0x40,0x20] decodes to row0=[0x80,0x90], row1=[0x40,0x60].
+ *
+ * Layout: header(8) + IFD(2+9*12+4) + strip(4) = 126 bytes
+ */
+static int test_predictor_gray8(void)
+{
+    uint8_t buf[126];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,   8);         /* BitsPerSample=8 */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 4, 1, 122);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,   4);         /* StripByteCounts */
+    ifd_le(buf, &p, 0x013D, 3, 1,   2);         /* Predictor=2 */
+    w32le(buf, &p, 0);
+
+    buf[p++] = 0x80; buf[p++] = 0x10;           /* row 0: base=0x80, 
delta=0x10 */
+    buf[p++] = 0x40; buf[p++] = 0x20;           /* row 1: base=0x40, 
delta=0x20 */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 RGB24 image.
+ * Exercises: multi-component BitsPerSample ([8,8,8], count=3 — off-IFD
+ * sum loop at lines 1329-1334), SamplesPerPixel=3 handler (1340-1350),
+ * and init_image case 243 non-YCbCr path (AV_PIX_FMT_RGB24, line 1139).
+ *
+ * Layout:
+ *   header(8) + IFD(2+9*12+4) + bps_data(6) + strip(12) = 140 bytes
+ *
+ * BitsPerSample=[8,8,8] at 122; RGB strip (4 pixels) at 128.
+ */
+static int test_rgb24(void)
+{
+    uint8_t buf[140];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 3, 122);         /* BitsPerSample=[8,8,8] at 
122 */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   2);         /* Photometric=RGB */
+    ifd_le(buf, &p, 0x0111, 4, 1, 128);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0115, 3, 1,   3);         /* SamplesPerPixel=3 */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,  12);         /* StripByteCounts=12 */
+    w32le(buf, &p, 0);
+
+    /* p == 122: BitsPerSample = [8, 8, 8] */
+    w16le(buf, &p, 8); w16le(buf, &p, 8); w16le(buf, &p, 8);
+
+    /* p == 128: RGB strip (4 pixels x 3 bytes) */
+    buf[p++]=0xFF; buf[p++]=0x00; buf[p++]=0x00; /* red */
+    buf[p++]=0x00; buf[p++]=0xFF; buf[p++]=0x00; /* green */
+    buf[p++]=0x00; buf[p++]=0x00; buf[p++]=0xFF; /* blue */
+    buf[p++]=0xFF; buf[p++]=0xFF; buf[p++]=0xFF; /* white */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 DNG image (RAW compression).
+ * The DNG_VERSION tag (0xC612, type BYTE, count=4, value=[1,3,0,0])
+ * triggers tiff_set_type(TIFF_TYPE_DNG), setting is_dng=1 in
+ * decode_frame.  This exercises the DNG matrix/premultiply setup
+ * (lines 2071-2122), the dng_blit() call inside tiff_unpack_strip,
+ * and the dng_process_color8/dng_process_color16 functions.
+ *
+ * Layout: header(8) + IFD(2 + 9*12 + 4) + pixel_data(4) = 126 bytes
+ */
+static int test_dng(void)
+{
+    uint8_t buf[126];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 3, 1, 122);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 3, 1,   4);         /* StripByteCounts */
+    /* DNG_VERSION = [1,3,0,0]: type BYTE(1), count=4, inline value 0x00000301 
*/
+    ifd_le(buf, &p, 0xC612, 1, 4, 0x00000301);
+    w32le(buf, &p, 0);
+
+    buf[p++] = 0xFF; buf[p++] = 0x80;
+    buf[p++] = 0x40; buf[p++] = 0x00;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with PackBits compression (0x8005).
+ * Row 0 uses a literal run (code=1 → copy 2 bytes).
+ * Row 1 uses a repeat run (code=-1 → fill 2 pixels with same byte).
+ * Exercises both branches of the PackBits decoder loop in
+ * tiff_unpack_strip (lines 922-958).
+ *
+ * Strip data (5 bytes): [0x01, 0xFF, 0x80, 0xFF, 0x40]
+ *   Row 0: 0x01 → literal 2 bytes [0xFF, 0x80]
+ *   Row 1: 0xFF (-1 as int8_t) → repeat 2x [0x40]
+ *
+ * Layout: header(8) + IFD(2 + 8*12 + 4) + strip(5) = 115 bytes
+ */
+static int test_packbits(void)
+{
+    uint8_t buf[115];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 8);                          /* 8 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3, 1, 32773);       /* Compression=PACKBITS 
(0x8005) */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 4, 1, 110);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,   5);         /* StripByteCounts=5 */
+    w32le(buf, &p, 0);
+
+    /* PackBits stream: literal run then repeat run */
+    buf[p++] = 0x01;                            /* copy next 2 bytes */
+    buf[p++] = 0xFF; buf[p++] = 0x80;           /* row 0 pixels */
+    buf[p++] = 0xFF;                            /* -1 as int8_t: repeat next 
byte 2x */
+    buf[p++] = 0x40;                            /* row 1 pixels */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with ADOBE_DEFLATE compression (type 8).
+ * The 4 raw pixels [0xFF,0x80,0x40,0x00] are stored as a zlib stream.
+ * Exercises tiff_uncompress() (inflate call) and tiff_unpack_zlib()
+ * (lines 501-564).
+ *
+ * Compressed bytes (zlib level 6, 12 bytes):
+ *   0x78 0x9C 0xFB 0xDF 0xE0 0xC0 0x00 0x00 0x06 0x00 0x01 0xC0
+ *
+ * Layout: header(8) + IFD(2 + 8*12 + 4) + zlib_data(12) = 122 bytes
+ */
+static int test_deflate(void)
+{
+    static const uint8_t zdata[12] = {
+        0x78, 0x9C, 0xFB, 0xDF, 0xE0, 0xC0, 0x00, 0x00, 0x06, 0x00, 0x01, 0xC0
+    };
+    uint8_t buf[122];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 8);                          /* 8 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3, 1,   8);         /* Compression=ADOBE_DEFLATE */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 4, 1, 110);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,  12);         /* StripByteCounts=12 */
+    w32le(buf, &p, 0);
+
+    memcpy(buf + p, zdata, sizeof(zdata));      /* zlib stream at offset 110 */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 GRAY8 with TIFF_GRAY_RESPONSE_CURVE (tag 0x0122).
+ * Exercises the GRAY_RESPONSE_CURVE / DNG_LINEARIZATION_TABLE handler
+ * (tiff.c lines 1463-1470) which populates s->dng_lut and sets s->white_level.
+ *
+ * Layout: header(8) + IFD(2 + 9*12 + 4) + strip(4) = 126 bytes
+ */
+static int test_gray_lut(void)
+{
+    uint8_t buf[126];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,   8);         /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   1);         /* Photometric=BLACK_IS_ZERO */
+    ifd_le(buf, &p, 0x0111, 4, 1, 122);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,   4);         /* StripByteCounts */
+    ifd_le(buf, &p, 0x0123, 3, 1, 255);         /* GRAY_RESPONSE_CURVE: 
count=1, value=255 */
+    w32le(buf, &p, 0);
+
+    buf[p++] = 0x10; buf[p++] = 0x20;
+    buf[p++] = 0x30; buf[p++] = 0x40;
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 RGB48LE image (16 bits per channel, 3 channels).
+ * BitsPerSample=[16,16,16] stored off-IFD at offset 122 (3*2=6 > 4 bytes).
+ * Exercises init_image case 483 (tiff.c lines 1162-1164) → AV_PIX_FMT_RGB48LE.
+ *
+ * Layout: header(8) + IFD(2 + 9*12 + 4) + BPS(6) + strip(24) = 152 bytes
+ */
+static int test_rgb48(void)
+{
+    uint8_t buf[152];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 3, 122);         /* BitsPerSample: 3 SHORTs at 
offset 122 */
+    ifd_le(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   2);         /* Photometric=RGB */
+    ifd_le(buf, &p, 0x0111, 4, 1, 128);         /* StripOffsets */
+    ifd_le(buf, &p, 0x0115, 3, 1,   3);         /* SamplesPerPixel */
+    ifd_le(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,  24);         /* StripByteCounts */
+    w32le(buf, &p, 0);
+
+    /* BPS data at offset 122: [16, 16, 16] */
+    w16le(buf, &p, 16); w16le(buf, &p, 16); w16le(buf, &p, 16);
+
+    /* strip: 2*2*6 = 24 zero bytes (already zeroed by memset) */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Big-endian 2x2 CMYK 16-bit image (PhotometricInterpretation=SEPARATED).
+ * BitsPerSample=[16,16,16,16] stored off-IFD at offset 122 (4*2=8 > 4 bytes).
+ * Exercises init_image case 644 (→ AV_PIX_FMT_RGBA64BE, tiff.c lines 
1165-1167)
+ * and the 16-bit CMYK-to-RGB conversion (tiff.c lines 2379-2394).
+ *
+ * Layout: header(8) + IFD(2 + 9*12 + 4) + BPS(8) + strip(32) = 162 bytes
+ */
+static int test_cmyk16be(void)
+{
+    uint8_t buf[162];
+    int p = 0;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x4D; buf[p++] = 0x4D;          /* 'MM' big-endian */
+    w16be(buf, &p, 42);
+    w32be(buf, &p, 8);
+
+    w16be(buf, &p, 9);                          /* 9 IFD entries */
+    ifd_be(buf, &p, 0x0100, 3, 1,   2);         /* ImageWidth */
+    ifd_be(buf, &p, 0x0101, 3, 1,   2);         /* ImageLength */
+    ifd_be(buf, &p, 0x0102, 3, 4, 122);         /* BitsPerSample: 4 SHORTs at 
offset 122 */
+    ifd_be(buf, &p, 0x0103, 3, 1,   1);         /* Compression=RAW */
+    ifd_be(buf, &p, 0x0106, 3, 1,   5);         /* Photometric=SEPARATED */
+    ifd_be(buf, &p, 0x0111, 4, 1, 130);         /* StripOffsets */
+    ifd_be(buf, &p, 0x0115, 3, 1,   4);         /* SamplesPerPixel */
+    ifd_be(buf, &p, 0x0116, 3, 1,   2);         /* RowsPerStrip */
+    ifd_be(buf, &p, 0x0117, 4, 1,  32);         /* StripByteCounts */
+    w32be(buf, &p, 0);
+
+    /* BPS data at offset 122: [16, 16, 16, 16] */
+    w16be(buf, &p, 16); w16be(buf, &p, 16); w16be(buf, &p, 16); w16be(buf, &p, 
16);
+
+    /* strip: 2*2*8 = 32 zero bytes (already zeroed by memset) */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+/*
+ * Little-endian 2x2 DNG Bayer RGGB 8-bit image.
+ * Exercises: CFA_PATTERN_DIM + CFA_PATTERN tag handlers, DNG_VERSION 
triggering
+ * TIFF_TYPE_DNG, DNG_BLACK_LEVEL, DNG_WHITE_LEVEL, DNG_COLOR_MATRIX1 (sets
+ * use_color_matrix=1), DNG_CAMERA_CALIBRATION1, init_image case 100081
+ * (→ AV_PIX_FMT_BAYER_RGGB8), camera_xyz_coeff(), and the use_color_matrix
+ * else-branch in decode_frame (tiff.c lines 2087-2096).
+ *
+ * Buffer layout:
+ *   header(8) + IFD count(2) + 16*12(192) + next_IFD(4) = 206 bytes
+ *   DNG_COLOR_MATRIX1 data  (9 SRATIONAL = 72 bytes) at offset 206
+ *   DNG_CAMERA_CALIBRATION1 data (72 bytes) at offset 278
+ *   strip (4 bytes) at offset 350
+ *   Total: 354 bytes
+ */
+static int test_bayer_rggb8(void)
+{
+    uint8_t buf[354];
+    int p = 0;
+    int r, c;
+
+    memset(buf, 0, sizeof(buf));
+
+    buf[p++] = 0x49; buf[p++] = 0x49;
+    w16le(buf, &p, 42);
+    w32le(buf, &p, 8);
+
+    w16le(buf, &p, 16);                              /* 16 IFD entries */
+    ifd_le(buf, &p, 0x0100, 3, 1,       2);          /* ImageWidth */
+    ifd_le(buf, &p, 0x0101, 3, 1,       2);          /* ImageLength */
+    ifd_le(buf, &p, 0x0102, 3, 1,       8);          /* BitsPerSample */
+    ifd_le(buf, &p, 0x0103, 3, 1,       1);          /* Compression=RAW */
+    ifd_le(buf, &p, 0x0106, 3, 1,   32803);          /* Photometric=CFA */
+    ifd_le(buf, &p, 0x0111, 4, 1,     350);          /* StripOffsets */
+    ifd_le(buf, &p, 0x0115, 3, 1,       1);          /* SamplesPerPixel */
+    ifd_le(buf, &p, 0x0116, 3, 1,       2);          /* RowsPerStrip */
+    ifd_le(buf, &p, 0x0117, 4, 1,       4);          /* StripByteCounts */
+    /* CFA_PATTERN_DIM: 2 inline SHORTs [2, 2] packed into the value field */
+    ifd_le(buf, &p, 0x828D, 3, 2, 0x00020002);       /* CFA pattern size 2x2 */
+    /* CFA_PATTERN: 4 inline BYTEs [0,1,1,2] = RGGB */
+    ifd_le(buf, &p, 0x828E, 1, 4, 0x02010100);       /* RGGB Bayer pattern */
+    /* DNG_VERSION: 4 inline BYTEs [1,3,0,0] */
+    ifd_le(buf, &p, 0xC612, 1, 4, 0x00000301);       /* DNG version 1.3.0.0 */
+    ifd_le(buf, &p, 0xC61A, 3, 1,       0);          /* DNG_BLACK_LEVEL=0 */
+    ifd_le(buf, &p, 0xC61D, 3, 1,     255);          /* DNG_WHITE_LEVEL=255 */
+    /* DNG_COLOR_MATRIX1: 9 SRATIONAL values (9*8=72 bytes) at offset 206 */
+    ifd_le(buf, &p, 0xC621, 10, 9,    206);          /* DNG_COLOR_MATRIX1 */
+    /* DNG_CAMERA_CALIBRATION1: 9 SRATIONAL values at offset 278 */
+    ifd_le(buf, &p, 0xC623, 10, 9,    278);          /* 
DNG_CAMERA_CALIBRATION1 */
+    w32le(buf, &p, 0);
+
+    /* DNG_COLOR_MATRIX1 at offset 206: identity 3x3 matrix as signed 
rationals */
+    for (r = 0; r < 3; r++)
+        for (c = 0; c < 3; c++) {
+            w32le(buf, &p, r == c ? 1 : 0);          /* numerator */
+            w32le(buf, &p, 1);                        /* denominator */
+        }
+
+    /* DNG_CAMERA_CALIBRATION1 at offset 278: identity 3x3 matrix */
+    for (r = 0; r < 3; r++)
+        for (c = 0; c < 3; c++) {
+            w32le(buf, &p, r == c ? 1 : 0);
+            w32le(buf, &p, 1);
+        }
+
+    /* strip: 4 bytes at offset 350 (zero pixels, already set by memset) */
+
+    return decode_tiff(buf, sizeof(buf));
+}
+
+#define TEST(name) do {                                                      \
+    int fmt = test_##name();                                                 \
+    const char *fn = fmt >= 0 ? av_get_pix_fmt_name(fmt) : NULL;            \
+    printf(#name ": %s\n", fn ? fn : "fail");                               \
+} while (0)
+
+int main(void)
+{
+    TEST(be_gray8);
+    TEST(be_gray16);
+    TEST(miniswhite);
+    TEST(pal8);
+    TEST(fill_order);
+    TEST(gray12);
+    TEST(metadata);
+    TEST(geotiff);
+    TEST(dng);
+    TEST(packbits);
+    TEST(deflate);
+    TEST(ycbcr);
+    TEST(cmyk);
+    TEST(more_metadata);
+    TEST(predictor_gray16le);
+    TEST(predictor_gray8);
+    TEST(rgb24);
+    TEST(gray_lut);
+    TEST(rgb48);
+    TEST(cmyk16be);
+    TEST(bayer_rggb8);
+    return 0;
+}
diff --git a/tests/fate/libavcodec.mak b/tests/fate/libavcodec.mak
index e2d616e307..f2c4d48558 100644
--- a/tests/fate/libavcodec.mak
+++ b/tests/fate/libavcodec.mak
@@ -114,5 +114,9 @@ FATE_LIBAVCODEC-yes += fate-libavcodec-htmlsubtitles
 fate-libavcodec-htmlsubtitles: libavcodec/tests/htmlsubtitles$(EXESUF)
 fate-libavcodec-htmlsubtitles: CMD = run 
libavcodec/tests/htmlsubtitles$(EXESUF)
 
+FATE_LIBAVCODEC-$(CONFIG_TIFF_DECODER) += fate-tiffdec
+fate-tiffdec: libavcodec/tests/tiffdec$(EXESUF)
+fate-tiffdec: CMD = run libavcodec/tests/tiffdec$(EXESUF)
+
 FATE-$(CONFIG_AVCODEC) += $(FATE_LIBAVCODEC-yes)
 fate-libavcodec: $(FATE_LIBAVCODEC-yes)
diff --git a/tests/ref/fate/tiffdec b/tests/ref/fate/tiffdec
new file mode 100644
index 0000000000..33bc20cf88
--- /dev/null
+++ b/tests/ref/fate/tiffdec
@@ -0,0 +1,21 @@
+be_gray8: gray
+be_gray16: gray16be
+miniswhite: gray
+pal8: pal8
+fill_order: gray
+gray12: gray12le
+metadata: gray
+geotiff: gray
+dng: gray
+packbits: gray
+deflate: gray
+ycbcr: yuv420p
+cmyk: rgb0
+more_metadata: gray
+predictor_gray16le: gray16le
+predictor_gray8: gray
+rgb24: rgb24
+gray_lut: gray
+rgb48: rgb48le
+cmyk16be: rgba64be
+bayer_rggb8: bayer_rggb8
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to