Commit 6c95ba04 for libheif

commit 6c95ba04a4fff1378ab846eaf4ae5edbb5a00b18
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri Feb 27 11:05:38 2026 +0100

    add loading TIFF files with float samples

diff --git a/heifio/decoder_tiff.cc b/heifio/decoder_tiff.cc
index 238bcf8a..ba3ce92d 100644
--- a/heifio/decoder_tiff.cc
+++ b/heifio/decoder_tiff.cc
@@ -38,6 +38,8 @@ extern "C" {
 }

 #include "decoder_tiff.h"
+#include "libheif/heif_experimental.h"
+#include "libheif/heif_uncompressed.h"

 static struct heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"};

@@ -545,12 +547,55 @@ heif_error readBandInterleave(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha
 }


+static heif_error readMonoFloat(TIFF* tif, heif_image** image)
+{
+  uint32_t width, height;
+  heif_error err = getImageWidthAndHeight(tif, width, height);
+  if (err.code != heif_error_Ok) {
+    return err;
+  }
+
+  err = heif_image_create((int)width, (int)height, heif_colorspace_nonvisual, heif_chroma_undefined, image);
+  if (err.code != heif_error_Ok) {
+    return err;
+  }
+
+  uint32_t component_idx;
+  err = heif_image_add_component(*image, (int)width, (int)height,
+                                 heif_uncompressed_component_type_monochrome,
+                                 heif_channel_datatype_floating_point, 32, &component_idx);
+  if (err.code != heif_error_Ok) {
+    heif_image_release(*image);
+    *image = nullptr;
+    return err;
+  }
+
+  size_t stride;
+  float* plane = heif_image_get_component_float32(*image, component_idx, &stride);
+  if (!plane) {
+    heif_image_release(*image);
+    *image = nullptr;
+    return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get float plane"};
+  }
+
+  tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif));
+  for (uint32_t row = 0; row < height; row++) {
+    TIFFReadScanline(tif, buf, row, 0);
+    memcpy(reinterpret_cast<uint8_t*>(plane) + row * stride, buf, width * sizeof(float));
+  }
+  _TIFFfree(buf);
+
+  return heif_error_ok;
+}
+
+
 static void suppress_warnings(const char* module, const char* fmt, va_list ap) {
   // Do nothing
 }


-static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint16_t& bps, uint16_t& config, bool& hasAlpha)
+static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint16_t& bps,
+                                     uint16_t& config, bool& hasAlpha, uint16_t& sampleFormat)
 {
   uint16_t shortv;
   if (TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &shortv) && shortv == PHOTOMETRIC_PALETTE) {
@@ -580,15 +625,30 @@ static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint1
   }

   TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bps);
-  if (bps != 8 && bps != 16) {
-    return {heif_error_Invalid_input, heif_suberror_Unspecified,
-            "Only 8 and 16 bits per sample are supported."};
+
+  if (!TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &sampleFormat)) {
+    sampleFormat = SAMPLEFORMAT_UINT;
   }

-  uint16_t format;
-  if (TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &format) && format != SAMPLEFORMAT_UINT) {
-    return {heif_error_Invalid_input, heif_suberror_Unspecified,
-            "Only UINT sample format is supported."};
+  if (sampleFormat == SAMPLEFORMAT_IEEEFP) {
+    if (bps != 32) {
+      return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+              "Only 32-bit floating point TIFF is supported."};
+    }
+    if (samplesPerPixel != 1) {
+      return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+              "Only monochrome floating point TIFF is supported."};
+    }
+  }
+  else if (sampleFormat == SAMPLEFORMAT_UINT) {
+    if (bps != 8 && bps != 16) {
+      return {heif_error_Invalid_input, heif_suberror_Unspecified,
+              "Only 8 and 16 bits per sample are supported."};
+    }
+  }
+  else {
+    return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+            "Unsupported TIFF sample format."};
   }

   return heif_error_ok;
@@ -598,8 +658,64 @@ static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint1
 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,
-                                  uint16_t bps, int output_bit_depth, heif_image** out_image)
+                                  uint16_t bps, int output_bit_depth,
+                                  uint16_t sampleFormat, heif_image** out_image)
 {
+  bool isFloat = (sampleFormat == SAMPLEFORMAT_IEEEFP);
+
+  if (isFloat) {
+    heif_error err = heif_image_create((int)width, (int)height, heif_colorspace_nonvisual, heif_chroma_undefined, out_image);
+    if (err.code != heif_error_Ok) return err;
+
+    uint32_t component_idx;
+    err = heif_image_add_component(*out_image, (int)width, (int)height,
+                                   heif_uncompressed_component_type_monochrome,
+                                   heif_channel_datatype_floating_point, 32, &component_idx);
+    if (err.code != heif_error_Ok) {
+      heif_image_release(*out_image);
+      *out_image = nullptr;
+      return err;
+    }
+
+    size_t out_stride;
+    float* out_plane = heif_image_get_component_float32(*out_image, component_idx, &out_stride);
+    if (!out_plane) {
+      heif_image_release(*out_image);
+      *out_image = nullptr;
+      return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get float plane"};
+    }
+
+    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 = reinterpret_cast<uint8_t*>(out_plane) + (ty * tile_height + row) * out_stride
+                         + tx * tile_width * sizeof(float);
+          float* src = reinterpret_cast<float*>(tile_buf.data() + row * tile_width * sizeof(float));
+          memcpy(dst, src, actual_w * sizeof(float));
+        }
+      }
+    }
+
+    return heif_error_ok;
+  }
+
   uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel;
   int effectiveBitDepth = (bps <= 8) ? 8 : output_bit_depth;
   heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth);
@@ -701,8 +817,16 @@ static heif_error readTiledContiguous(TIFF* tif, uint32_t width, uint32_t height
 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,
-                                    uint16_t bps, int output_bit_depth, heif_image** out_image)
+                                    uint16_t bps, int output_bit_depth,
+                                    uint16_t sampleFormat, heif_image** out_image)
 {
+  // For mono float, separate layout is the same as contiguous (1 sample)
+  if (sampleFormat == SAMPLEFORMAT_IEEEFP) {
+    return readTiledContiguous(tif, width, height, tile_width, tile_height,
+                               samplesPerPixel, hasAlpha, bps, output_bit_depth,
+                               sampleFormat, out_image);
+  }
+
   uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel;
   int effectiveBitDepth = (bps <= 8) ? 8 : output_bit_depth;
   heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth);
@@ -784,13 +908,15 @@ heif_error loadTIFF(const char* filename, int output_bit_depth, InputImage *inpu

   TIFF* tif = tifPtr.get();

-  uint16_t samplesPerPixel, bps, config;
+  uint16_t samplesPerPixel, bps, config, sampleFormat;
   bool hasAlpha;
-  heif_error err = validateTiffFormat(tif, samplesPerPixel, bps, config, hasAlpha);
+  heif_error err = validateTiffFormat(tif, samplesPerPixel, bps, config, hasAlpha, sampleFormat);
   if (err.code != heif_error_Ok) return err;

+  bool isFloat = (sampleFormat == SAMPLEFORMAT_IEEEFP);
+
   // For 8-bit source, always produce 8-bit output (ignore output_bit_depth).
-  int effectiveOutputBitDepth = (bps <= 8) ? 8 : output_bit_depth;
+  int effectiveOutputBitDepth = isFloat ? 32 : ((bps <= 8) ? 8 : output_bit_depth);

   struct heif_image* image = nullptr;

@@ -806,25 +932,30 @@ heif_error loadTIFF(const char* filename, int output_bit_depth, InputImage *inpu

     switch (config) {
       case PLANARCONFIG_CONTIG:
-        err = readTiledContiguous(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
+        err = readTiledContiguous(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, sampleFormat, &image);
         break;
       case PLANARCONFIG_SEPARATE:
-        err = readTiledSeparate(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
+        err = readTiledSeparate(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, sampleFormat, &image);
         break;
       default:
         return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"};
     }
   }
   else {
-    switch (config) {
-      case PLANARCONFIG_CONTIG:
-        err = readPixelInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
-        break;
-      case PLANARCONFIG_SEPARATE:
-        err = readBandInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
-        break;
-      default:
-        return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"};
+    if (isFloat) {
+      err = readMonoFloat(tif, &image);
+    }
+    else {
+      switch (config) {
+        case PLANARCONFIG_CONTIG:
+          err = readPixelInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
+          break;
+        case PLANARCONFIG_SEPARATE:
+          err = readBandInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
+          break;
+        default:
+          return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"};
+      }
     }
   }

@@ -875,7 +1006,7 @@ std::unique_ptr<TiledTiffReader> TiledTiffReader::open(const char* filename, hei
   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);
+  heif_error err = validateTiffFormat(tif, reader->m_samples_per_pixel, bps, reader->m_planar_config, reader->m_has_alpha, reader->m_sample_format);
   if (err.code != heif_error_Ok) {
     *out_err = err;
     return nullptr;
@@ -914,9 +1045,9 @@ std::unique_ptr<TiledTiffReader> TiledTiffReader::open(const char* filename, hei
       continue;
     }

-    uint16_t spp, bps_ov, config_ov;
+    uint16_t spp, bps_ov, config_ov, sampleFormat_ov;
     bool hasAlpha_ov;
-    heif_error valErr = validateTiffFormat(tif, spp, bps_ov, config_ov, hasAlpha_ov);
+    heif_error valErr = validateTiffFormat(tif, spp, bps_ov, config_ov, hasAlpha_ov, sampleFormat_ov);
     if (valErr.code != heif_error_Ok) {
       continue;
     }
@@ -964,7 +1095,7 @@ bool TiledTiffReader::setDirectory(uint32_t dir_index)
   }

   uint16_t bps;
-  heif_error err = validateTiffFormat(tif, m_samples_per_pixel, bps, m_planar_config, m_has_alpha);
+  heif_error err = validateTiffFormat(tif, m_samples_per_pixel, bps, m_planar_config, m_has_alpha, m_sample_format);
   if (err.code != heif_error_Ok) {
     return false;
   }
@@ -984,6 +1115,48 @@ heif_error TiledTiffReader::readTile(uint32_t tx, uint32_t ty, int output_bit_de
   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);

+  if (m_sample_format == SAMPLEFORMAT_IEEEFP) {
+    heif_error err = heif_image_create((int)actual_w, (int)actual_h, heif_colorspace_nonvisual, heif_chroma_undefined, out_image);
+    if (err.code != heif_error_Ok) return err;
+
+    uint32_t component_idx;
+    err = heif_image_add_component(*out_image, (int)actual_w, (int)actual_h,
+                                   heif_uncompressed_component_type_monochrome,
+                                   heif_channel_datatype_floating_point, 32, &component_idx);
+    if (err.code != heif_error_Ok) {
+      heif_image_release(*out_image);
+      *out_image = nullptr;
+      return err;
+    }
+
+    size_t out_stride;
+    float* out_plane = heif_image_get_component_float32(*out_image, component_idx, &out_stride);
+    if (!out_plane) {
+      heif_image_release(*out_image);
+      *out_image = nullptr;
+      return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get float plane"};
+    }
+
+    tmsize_t tile_buf_size = TIFFTileSize(tif);
+    std::vector<uint8_t> tile_buf(tile_buf_size);
+
+    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 = reinterpret_cast<uint8_t*>(out_plane) + row * out_stride;
+      float* src = reinterpret_cast<float*>(tile_buf.data() + row * m_tile_width * sizeof(float));
+      memcpy(dst, src, actual_w * sizeof(float));
+    }
+
+    return heif_error_ok;
+  }
+
   int effectiveBitDepth = (m_bits_per_sample <= 8) ? 8 : output_bit_depth;
   uint16_t outSpp = (m_samples_per_pixel == 4 && !m_has_alpha) ? 3 : m_samples_per_pixel;
   heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth);
diff --git a/heifio/decoder_tiff.h b/heifio/decoder_tiff.h
index 71598267..ebdf6575 100644
--- a/heifio/decoder_tiff.h
+++ b/heifio/decoder_tiff.h
@@ -29,6 +29,7 @@

 #include "decoder.h"
 #include "libheif/heif.h"
+#include "libheif/heif_experimental.h"
 #include <memory>
 #include <cstdint>
 #include <vector>
@@ -63,6 +64,7 @@ public:
   bool setDirectory(uint32_t dir_index);

   uint16_t bitsPerSample() const { return m_bits_per_sample; }
+  uint16_t sampleFormat() const { return m_sample_format; }

   heif_error readTile(uint32_t tx, uint32_t ty, int output_bit_depth, heif_image** out_image);
   void readExif(InputImage* input_image);
@@ -79,6 +81,7 @@ private:
   uint16_t m_samples_per_pixel = 0;
   uint16_t m_bits_per_sample = 0;
   uint16_t m_planar_config = 0;
+  uint16_t m_sample_format = 1; // SAMPLEFORMAT_UINT
   bool m_has_alpha = false;

   std::vector<OverviewInfo> m_overviews;