Commit e5540c04 for libheif

commit e5540c042d9fec7e4d5863279182c83f24f364f9
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Feb 16 14:27:54 2026 +0100

    added a reader for tiled TIFFs, keeping the same tiling in the HEIF

diff --git a/examples/heif_enc.cc b/examples/heif_enc.cc
index f13a377b..32a6c3d7 100644
--- a/examples/heif_enc.cc
+++ b/examples/heif_enc.cc
@@ -1037,6 +1037,46 @@ private:
 };


+#if HAVE_LIBTIFF
+class input_tiles_generator_tiff : public input_tiles_generator
+{
+public:
+  input_tiles_generator_tiff(std::unique_ptr<TiledTiffReader> reader)
+    : m_reader(std::move(reader))
+  {
+  }
+
+  uint32_t nColumns() const override { return m_reader->nColumns(); }
+  uint32_t nRows() const override { return m_reader->nRows(); }
+
+  InputImage get_image(uint32_t tx, uint32_t ty, int /*output_bit_depth*/) override
+  {
+    heif_image* tile_image = nullptr;
+    heif_error err = m_reader->readTile(tx, ty, &tile_image);
+    if (err.code != heif_error_Ok) {
+      std::cerr << "Error reading TIFF tile " << tx << "," << ty << ": " << err.message << "\n";
+      exit(1);
+    }
+
+    InputImage input;
+    input.image = std::shared_ptr<heif_image>(tile_image,
+                                               [](heif_image* img) { heif_image_release(img); });
+    return input;
+  }
+
+  uint32_t imageWidth() const { return m_reader->imageWidth(); }
+  uint32_t imageHeight() const { return m_reader->imageHeight(); }
+  uint32_t tileWidth() const { return m_reader->tileWidth(); }
+  uint32_t tileHeight() const { return m_reader->tileHeight(); }
+
+  void readExif(InputImage* input_image) { m_reader->readExif(input_image); }
+
+private:
+  std::unique_ptr<TiledTiffReader> m_reader;
+};
+#endif
+
+
 // TODO: we have to attach the input image Exif and XMP to the tiled image
 heif_image_handle* encode_tiled(heif_context* ctx, heif_encoder* encoder, heif_encoding_options* options,
                                 int output_bit_depth,
@@ -1830,12 +1870,57 @@ int do_encode_images(heif_context* context, heif_encoder* encoder, heif_encoding

   for (std::string input_filename : args) {

-    InputImage input_image = load_image(input_filename, output_bit_depth);
+    InputImage input_image;
+    heif_image_tiling tiling{};
+    std::shared_ptr<input_tiles_generator> tile_generator;
+
+#if HAVE_LIBTIFF
+    // Auto-detect tiled TIFFs when not using explicit tiling options
+    if (!use_tiling && cut_tiles == 0) {
+      std::string suffix;
+      auto suffix_pos = input_filename.find_last_of('.');
+      if (suffix_pos != std::string::npos) {
+        suffix = input_filename.substr(suffix_pos + 1);
+        std::transform(suffix.begin(), suffix.end(), suffix.begin(), ::tolower);
+      }
+
+      if (suffix == "tif" || suffix == "tiff") {
+        heif_error tiff_err;
+        auto tiff_reader = TiledTiffReader::open(input_filename.c_str(), &tiff_err);
+        if (tiff_err.code != heif_error_Ok) {
+          std::cerr << "Error opening TIFF: " << tiff_err.message << "\n";
+          return 1;
+        }
+
+        if (tiff_reader) {
+          auto tiff_gen = std::make_shared<input_tiles_generator_tiff>(std::move(tiff_reader));
+
+          // Read tile (0,0) as representative for nclx profile
+          input_image = tiff_gen->get_image(0, 0, output_bit_depth);
+          tiff_gen->readExif(&input_image);
+
+          tiling.version = 1;
+          tiling.num_columns = tiff_gen->nColumns();
+          tiling.num_rows = tiff_gen->nRows();
+          tiling.tile_width = tiff_gen->tileWidth();
+          tiling.tile_height = tiff_gen->tileHeight();
+          tiling.image_width = tiff_gen->imageWidth();
+          tiling.image_height = tiff_gen->imageHeight();
+          tiling.number_of_extra_dimensions = 0;
+
+          tile_generator = tiff_gen;
+        }
+      }
+    }
+#endif
+
+    // If no tiled TIFF was detected, load the image normally
+    if (!input_image.image) {
+      input_image = load_image(input_filename, output_bit_depth);
+    }

     std::shared_ptr<heif_image> image = input_image.image;

-    heif_image_tiling tiling{};
-    std::shared_ptr<input_tiles_generator> tile_generator;
     if (use_tiling) {
       tile_generator = determine_input_images_tiling(input_filename, tiled_input_x_y);
       if (tile_generator) {
@@ -1900,7 +1985,7 @@ int do_encode_images(heif_context* context, heif_encoder* encoder, heif_encoding

     heif_image_handle* handle;

-    if (use_tiling || cut_tiles > 0) {
+    if (tile_generator) {
       handle = encode_tiled(context, encoder, options, output_bit_depth, tile_generator, tiling);
     }
     else {
diff --git a/heifio/decoder_tiff.cc b/heifio/decoder_tiff.cc
index 5f9ccb56..246aa86d 100644
--- a/heifio/decoder_tiff.cc
+++ b/heifio/decoder_tiff.cc
@@ -399,80 +399,215 @@ static void suppress_warnings(const char* module, const char* fmt, va_list ap) {
 }


-heif_error loadTIFF(const char* filename, InputImage *input_image) {
-  TIFFSetWarningHandler(suppress_warnings);
-
-  std::unique_ptr<TIFF, void(*)(TIFF*)> tifPtr(TIFFOpen(filename, "r"), [](TIFF* tif) { TIFFClose(tif); });
-  if (!tifPtr) {
-    struct heif_error err = {
-      .code = heif_error_Invalid_input,
-      .subcode = heif_suberror_Unspecified,
-      .message = "Cannot open TIFF ile"};
-    return err;
-  }
-
-  TIFF* tif = tifPtr.get();
-  if (TIFFIsTiled(tif)) {
-    struct heif_error err = {
-      .code = heif_error_Unsupported_feature,
-      .subcode = heif_suberror_Unspecified,
-      .message = "Tiled TIFF images are not supported yet"};
-    return err;
-  }
-
-  uint16_t shortv, samplesPerPixel, bps, config, format;
+static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint16_t& bps, uint16_t& config, bool& hasAlpha)
+{
+  uint16_t shortv;
   if (TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &shortv) && shortv == PHOTOMETRIC_PALETTE) {
-    struct heif_error err = {
-      .code = heif_error_Unsupported_feature,
-      .subcode = heif_suberror_Unspecified,
-      .message = "Palette TIFF images are not supported yet"};
-    return err;
+    return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+            "Palette TIFF images are not supported yet"};
   }

   TIFFGetField(tif, TIFFTAG_PLANARCONFIG, &config);
   TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel);
   if (samplesPerPixel != 1 && samplesPerPixel != 3 && samplesPerPixel != 4) {
-    struct heif_error err = {
-      .code = heif_error_Invalid_input,
-      .subcode = heif_suberror_Unspecified,
-      .message = "Only 1, 3 and 4 samples per pixel are supported."};
-    return err;
+    return {heif_error_Invalid_input, heif_suberror_Unspecified,
+            "Only 1, 3 and 4 samples per pixel are supported."};
+  }
+
+  // Determine whether the 4th sample is true alpha or an unrelated extra sample
+  hasAlpha = false;
+  if (samplesPerPixel == 4) {
+    uint16_t extraCount = 0;
+    uint16_t* extraTypes = nullptr;
+    if (TIFFGetField(tif, TIFFTAG_EXTRASAMPLES, &extraCount, &extraTypes) && extraCount > 0) {
+      hasAlpha = (extraTypes[0] == EXTRASAMPLE_ASSOCALPHA || extraTypes[0] == EXTRASAMPLE_UNASSALPHA);
+    }
+    else {
+      // No EXTRASAMPLES tag with 4 spp — assume RGBA for backward compatibility
+      hasAlpha = true;
+    }
   }

   TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bps);
-  if (bps != 8) {
-    struct heif_error err = {
-      .code = heif_error_Invalid_input,
-      .subcode = heif_suberror_Unspecified,
-      .message = "Only 8 bits per sample are supported."};
-    return err;
+  if (bps != 8) {
+    return {heif_error_Invalid_input, heif_suberror_Unspecified,
+            "Only 8 bits per sample are supported."};
   }

+  uint16_t format;
   if (TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &format) && format != SAMPLEFORMAT_UINT) {
-    struct heif_error err = {
-      .code = heif_error_Invalid_input,
-      .subcode = heif_suberror_Unspecified,
-      .message = "Only UINT sample format is supported."};
-    return err;
+    return {heif_error_Invalid_input, heif_suberror_Unspecified,
+            "Only UINT sample format is supported."};
+  }
+
+  return heif_error_ok;
+}
+
+
+static heif_error readTiledContiguous(TIFF* tif, uint32_t width, uint32_t height,
+                                  uint32_t tile_width, uint32_t tile_height,
+                                  uint16_t samplesPerPixel, bool hasAlpha, heif_image** out_image)
+{
+  uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel;
+  heif_chroma chroma = (outSpp == 1) ? heif_chroma_monochrome
+                       : (outSpp == 4) ? heif_chroma_interleaved_RGBA
+                       : heif_chroma_interleaved_RGB;
+  heif_colorspace colorspace = (outSpp == 1) ? heif_colorspace_monochrome : heif_colorspace_RGB;
+  heif_channel channel = (outSpp == 1) ? heif_channel_Y : heif_channel_interleaved;
+
+  heif_error err = heif_image_create((int)width, (int)height, colorspace, chroma, out_image);
+  if (err.code != heif_error_Ok) return err;
+
+  heif_image_add_plane(*out_image, channel, (int)width, (int)height, outSpp * 8);
+
+  size_t out_stride;
+  uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride);
+
+  tmsize_t tile_buf_size = TIFFTileSize(tif);
+  std::vector<uint8_t> tile_buf(tile_buf_size);
+
+  uint32_t n_cols = (width + tile_width - 1) / tile_width;
+  uint32_t n_rows = (height + tile_height - 1) / tile_height;
+
+  for (uint32_t ty = 0; ty < n_rows; ty++) {
+    for (uint32_t tx = 0; tx < n_cols; tx++) {
+      tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * tile_width, ty * tile_height, 0, 0),
+                                          tile_buf.data(), tile_buf_size);
+      if (read < 0) {
+        heif_image_release(*out_image);
+        *out_image = nullptr;
+        return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"};
+      }
+
+      uint32_t actual_w = std::min(tile_width, width - tx * tile_width);
+      uint32_t actual_h = std::min(tile_height, height - ty * tile_height);
+
+      for (uint32_t row = 0; row < actual_h; row++) {
+        uint8_t* dst = out_plane + (ty * tile_height + row) * out_stride + tx * tile_width * outSpp;
+        uint8_t* src = tile_buf.data() + row * tile_width * samplesPerPixel;
+        if (outSpp == samplesPerPixel) {
+          memcpy(dst, src, actual_w * outSpp);
+        }
+        else {
+          // Strip extra sample (RGBX -> RGB)
+          for (uint32_t x = 0; x < actual_w; x++) {
+            memcpy(dst + x * outSpp, src + x * samplesPerPixel, outSpp);
+          }
+        }
+      }
+    }
+  }
+
+  return heif_error_ok;
+}
+
+
+static heif_error readTiledSeparate(TIFF* tif, uint32_t width, uint32_t height,
+                                    uint32_t tile_width, uint32_t tile_height,
+                                    uint16_t samplesPerPixel, bool hasAlpha, heif_image** out_image)
+{
+  uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel;
+  heif_chroma chroma = (outSpp == 1) ? heif_chroma_monochrome
+                       : (outSpp == 4) ? heif_chroma_interleaved_RGBA
+                       : heif_chroma_interleaved_RGB;
+  heif_colorspace colorspace = (outSpp == 1) ? heif_colorspace_monochrome : heif_colorspace_RGB;
+  heif_channel channel = (outSpp == 1) ? heif_channel_Y : heif_channel_interleaved;
+
+  heif_error err = heif_image_create((int)width, (int)height, colorspace, chroma, out_image);
+  if (err.code != heif_error_Ok) return err;
+
+  heif_image_add_plane(*out_image, channel, (int)width, (int)height, outSpp * 8);
+
+  size_t out_stride;
+  uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride);
+
+  tmsize_t tile_buf_size = TIFFTileSize(tif);
+  std::vector<uint8_t> tile_buf(tile_buf_size);
+
+  uint32_t n_cols = (width + tile_width - 1) / tile_width;
+  uint32_t n_rows = (height + tile_height - 1) / tile_height;
+
+  // Only interleave the first outSpp planes (skip the extra sample plane if !hasAlpha)
+  for (uint16_t s = 0; s < outSpp; s++) {
+    for (uint32_t ty = 0; ty < n_rows; ty++) {
+      for (uint32_t tx = 0; tx < n_cols; tx++) {
+        tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * tile_width, ty * tile_height, 0, s),
+                                            tile_buf.data(), tile_buf_size);
+        if (read < 0) {
+          heif_image_release(*out_image);
+          *out_image = nullptr;
+          return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"};
+        }
+
+        uint32_t actual_w = std::min(tile_width, width - tx * tile_width);
+        uint32_t actual_h = std::min(tile_height, height - ty * tile_height);
+
+        for (uint32_t row = 0; row < actual_h; row++) {
+          uint8_t* dst = out_plane + (ty * tile_height + row) * out_stride + tx * tile_width * outSpp + s;
+          uint8_t* src = tile_buf.data() + row * tile_width;
+          for (uint32_t x = 0; x < actual_w; x++) {
+            dst[x * outSpp] = src[x];
+          }
+        }
+      }
+    }
+  }
+
+  return heif_error_ok;
+}
+
+
+heif_error loadTIFF(const char* filename, InputImage *input_image) {
+  TIFFSetWarningHandler(suppress_warnings);
+
+  std::unique_ptr<TIFF, void(*)(TIFF*)> tifPtr(TIFFOpen(filename, "r"), [](TIFF* tif) { TIFFClose(tif); });
+  if (!tifPtr) {
+    return {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot open TIFF file"};
   }

-  struct heif_error err;
+  TIFF* tif = tifPtr.get();
+
+  uint16_t samplesPerPixel, bps, config;
+  bool hasAlpha;
+  heif_error err = validateTiffFormat(tif, samplesPerPixel, bps, config, hasAlpha);
+  if (err.code != heif_error_Ok) return err;
+
   struct heif_image* image = nullptr;

-  switch (config) {
-    case PLANARCONFIG_CONTIG:
-      err = readPixelInterleave(tif, samplesPerPixel, &image);
-      break;
-    case PLANARCONFIG_SEPARATE:
-      err = readBandInterleave(tif, samplesPerPixel, &image);
-      break;
-    default:
-      struct heif_error err = {
-        .code = heif_error_Invalid_input,
-        .subcode = heif_suberror_Unspecified,
-        .message = "Unsupported planar configuration"};
-      return err;
+  if (TIFFIsTiled(tif)) {
+    uint32_t width, height, tile_width, tile_height;
+    err = getImageWidthAndHeight(tif, width, height);
+    if (err.code != heif_error_Ok) return err;
+
+    if (!TIFFGetField(tif, TIFFTAG_TILEWIDTH, &tile_width) ||
+        !TIFFGetField(tif, TIFFTAG_TILELENGTH, &tile_height)) {
+      return {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot read TIFF tile dimensions"};
+    }
+
+    switch (config) {
+      case PLANARCONFIG_CONTIG:
+        err = readTiledContiguous(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, &image);
+        break;
+      case PLANARCONFIG_SEPARATE:
+        err = readTiledSeparate(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, &image);
+        break;
+      default:
+        return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"};
+    }
   }
+  else {
+    switch (config) {
+      case PLANARCONFIG_CONTIG:
+        err = readPixelInterleave(tif, samplesPerPixel, &image);
+        break;
+      case PLANARCONFIG_SEPARATE:
+        err = readBandInterleave(tif, samplesPerPixel, &image);
+        break;
+      default:
+        return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"};
+    }
+  }
+
   if (err.code != heif_error_Ok) {
     return err;
   }
@@ -490,3 +625,144 @@ heif_error loadTIFF(const char* filename, InputImage *input_image) {
   return heif_error_ok;
 }

+
+// --- TiledTiffReader ---
+
+void TiledTiffReader::TiffCloser::operator()(void* tif) const {
+  if (tif) {
+    TIFFClose(static_cast<TIFF*>(tif));
+  }
+}
+
+
+std::unique_ptr<TiledTiffReader> TiledTiffReader::open(const char* filename, heif_error* out_err)
+{
+  TIFFSetWarningHandler(suppress_warnings);
+
+  TIFF* tif = TIFFOpen(filename, "r");
+  if (!tif) {
+    *out_err = {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot open TIFF file"};
+    return nullptr;
+  }
+
+  if (!TIFFIsTiled(tif)) {
+    TIFFClose(tif);
+    *out_err = heif_error_ok;
+    return nullptr;
+  }
+
+  auto reader = std::unique_ptr<TiledTiffReader>(new TiledTiffReader());
+  reader->m_tif.reset(tif);
+
+  uint16_t bps;
+  heif_error err = validateTiffFormat(tif, reader->m_samples_per_pixel, bps, reader->m_planar_config, reader->m_has_alpha);
+  if (err.code != heif_error_Ok) {
+    *out_err = err;
+    return nullptr;
+  }
+  reader->m_bits_per_sample = bps;
+
+  if (!TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &reader->m_image_width) ||
+      !TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &reader->m_image_height)) {
+    *out_err = {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot read TIFF image dimensions"};
+    return nullptr;
+  }
+
+  if (!TIFFGetField(tif, TIFFTAG_TILEWIDTH, &reader->m_tile_width) ||
+      !TIFFGetField(tif, TIFFTAG_TILELENGTH, &reader->m_tile_height)) {
+    *out_err = {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot read TIFF tile dimensions"};
+    return nullptr;
+  }
+
+  reader->m_n_columns = (reader->m_image_width + reader->m_tile_width - 1) / reader->m_tile_width;
+  reader->m_n_rows = (reader->m_image_height + reader->m_tile_height - 1) / reader->m_tile_height;
+
+  *out_err = heif_error_ok;
+  return reader;
+}
+
+
+TiledTiffReader::~TiledTiffReader() = default;
+
+
+heif_error TiledTiffReader::readTile(uint32_t tx, uint32_t ty, heif_image** out_image)
+{
+  TIFF* tif = static_cast<TIFF*>(m_tif.get());
+
+  uint32_t actual_w = std::min(m_tile_width, m_image_width - tx * m_tile_width);
+  uint32_t actual_h = std::min(m_tile_height, m_image_height - ty * m_tile_height);
+
+  uint16_t outSpp = (m_samples_per_pixel == 4 && !m_has_alpha) ? 3 : m_samples_per_pixel;
+  heif_chroma chroma = (outSpp == 1) ? heif_chroma_monochrome
+                       : (outSpp == 4) ? heif_chroma_interleaved_RGBA
+                       : heif_chroma_interleaved_RGB;
+  heif_colorspace colorspace = (outSpp == 1) ? heif_colorspace_monochrome : heif_colorspace_RGB;
+  heif_channel channel = (outSpp == 1) ? heif_channel_Y : heif_channel_interleaved;
+
+  heif_error err = heif_image_create((int)actual_w, (int)actual_h, colorspace, chroma, out_image);
+  if (err.code != heif_error_Ok) return err;
+
+  heif_image_add_plane(*out_image, channel, (int)actual_w, (int)actual_h, outSpp * 8);
+
+  size_t out_stride;
+  uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride);
+
+  tmsize_t tile_buf_size = TIFFTileSize(tif);
+  std::vector<uint8_t> tile_buf(tile_buf_size);
+
+  if (m_planar_config == PLANARCONFIG_CONTIG) {
+    tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * m_tile_width, ty * m_tile_height, 0, 0),
+                                        tile_buf.data(), tile_buf_size);
+    if (read < 0) {
+      heif_image_release(*out_image);
+      *out_image = nullptr;
+      return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"};
+    }
+
+    for (uint32_t row = 0; row < actual_h; row++) {
+      uint8_t* dst = out_plane + row * out_stride;
+      uint8_t* src = tile_buf.data() + row * m_tile_width * m_samples_per_pixel;
+      if (outSpp == m_samples_per_pixel) {
+        memcpy(dst, src, actual_w * outSpp);
+      }
+      else {
+        for (uint32_t x = 0; x < actual_w; x++) {
+          memcpy(dst + x * outSpp, src + x * m_samples_per_pixel, outSpp);
+        }
+      }
+    }
+  }
+  else {
+    // PLANARCONFIG_SEPARATE: only read the first outSpp planes
+    for (uint16_t s = 0; s < outSpp; s++) {
+      tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * m_tile_width, ty * m_tile_height, 0, s),
+                                          tile_buf.data(), tile_buf_size);
+      if (read < 0) {
+        heif_image_release(*out_image);
+        *out_image = nullptr;
+        return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"};
+      }
+
+      for (uint32_t row = 0; row < actual_h; row++) {
+        uint8_t* dst = out_plane + row * out_stride + s;
+        uint8_t* src = tile_buf.data() + row * m_tile_width;
+        for (uint32_t x = 0; x < actual_w; x++) {
+          dst[x * outSpp] = src[x];
+        }
+      }
+    }
+  }
+
+  return heif_error_ok;
+}
+
+
+void TiledTiffReader::readExif(InputImage* input_image)
+{
+  TIFF* tif = static_cast<TIFF*>(m_tif.get());
+  std::unique_ptr<ExifTags> tags = ExifTags::Parse(tif);
+  if (tags) {
+    tags->Encode(&(input_image->exif));
+  }
+}
+
diff --git a/heifio/decoder_tiff.h b/heifio/decoder_tiff.h
index 81d04e2b..94cc2bc1 100644
--- a/heifio/decoder_tiff.h
+++ b/heifio/decoder_tiff.h
@@ -29,8 +29,43 @@

 #include "decoder.h"
 #include "libheif/heif.h"
+#include <memory>
+#include <cstdint>

 LIBHEIF_API
 heif_error loadTIFF(const char *filename, InputImage *input_image);

+class LIBHEIF_API TiledTiffReader {
+public:
+  ~TiledTiffReader();
+
+  // Returns a reader if the file is a tiled TIFF. If the TIFF is not tiled,
+  // returns nullptr with heif_error_Ok (caller should fall back to loadTIFF).
+  static std::unique_ptr<TiledTiffReader> open(const char* filename, heif_error* out_err);
+
+  uint32_t imageWidth() const { return m_image_width; }
+  uint32_t imageHeight() const { return m_image_height; }
+  uint32_t tileWidth() const { return m_tile_width; }
+  uint32_t tileHeight() const { return m_tile_height; }
+  uint32_t nColumns() const { return m_n_columns; }
+  uint32_t nRows() const { return m_n_rows; }
+
+  heif_error readTile(uint32_t tx, uint32_t ty, heif_image** out_image);
+  void readExif(InputImage* input_image);
+
+private:
+  TiledTiffReader() = default;
+
+  struct TiffCloser { void operator()(void* tif) const; };
+  std::unique_ptr<void, TiffCloser> m_tif;
+
+  uint32_t m_image_width = 0, m_image_height = 0;
+  uint32_t m_tile_width = 0, m_tile_height = 0;
+  uint32_t m_n_columns = 0, m_n_rows = 0;
+  uint16_t m_samples_per_pixel = 0;
+  uint16_t m_bits_per_sample = 0;
+  uint16_t m_planar_config = 0;
+  bool m_has_alpha = false;
+};
+
 #endif // LIBHEIF_DECODER_TIFF_H