Commit 9c4d1e0a for libheif

commit 9c4d1e0ac4b981050fc0232e5c2744a71d707c60
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri Feb 20 22:52:58 2026 +0100

    load 16bit TIFF files

diff --git a/examples/heif_enc.cc b/examples/heif_enc.cc
index 32a6c3d7..2f77f3b4 100644
--- a/examples/heif_enc.cc
+++ b/examples/heif_enc.cc
@@ -711,7 +711,7 @@ InputImage load_image(const std::string& input_filename, int output_bit_depth)
     }
   }
   else if (filetype == TIFF) {
-    heif_error err = loadTIFF(input_filename.c_str(), &input_image);
+    heif_error err = loadTIFF(input_filename.c_str(), output_bit_depth, &input_image);
     if (err.code != heif_error_Ok) {
       std::cerr << "Can not load TIFF input image: " << err.message << '\n';
       exit(1);
@@ -1049,10 +1049,10 @@ public:
   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
+  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);
+    heif_error err = m_reader->readTile(tx, ty, output_bit_depth, &tile_image);
     if (err.code != heif_error_Ok) {
       std::cerr << "Error reading TIFF tile " << tx << "," << ty << ": " << err.message << "\n";
       exit(1);
diff --git a/heifio/decoder_tiff.cc b/heifio/decoder_tiff.cc
index 1f0ddd24..b73379b3 100644
--- a/heifio/decoder_tiff.cc
+++ b/heifio/decoder_tiff.cc
@@ -258,6 +258,22 @@ void ExifTags::Encode(std::vector<uint8_t>* dest) {
   }
 }

+static heif_chroma get_heif_chroma(uint16_t outSpp, int output_bit_depth)
+{
+  if (outSpp == 1) {
+    return heif_chroma_monochrome;
+  }
+  if (output_bit_depth > 8) {
+#if IS_BIG_ENDIAN
+    return (outSpp == 4) ? heif_chroma_interleaved_RRGGBBAA_BE : heif_chroma_interleaved_RRGGBB_BE;
+#else
+    return (outSpp == 4) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBB_LE;
+#endif
+  }
+  return (outSpp == 4) ? heif_chroma_interleaved_RGBA : heif_chroma_interleaved_RGB;
+}
+
+
 heif_error getImageWidthAndHeight(TIFF *tif, uint32_t &width, uint32_t &height)
 {
   if (!TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width) ||
@@ -272,7 +288,7 @@ heif_error getImageWidthAndHeight(TIFF *tif, uint32_t &width, uint32_t &height)
   return heif_error_ok;
 }

-heif_error readMono(TIFF *tif, heif_image **image)
+heif_error readMono(TIFF *tif, uint16_t bps, int output_bit_depth, heif_image **image)
 {
   uint32_t width, height;
   heif_error err = getImageWidthAndHeight(tif, width, height);
@@ -283,109 +299,242 @@ heif_error readMono(TIFF *tif, heif_image **image)
   if (err.code != heif_error_Ok) {
     return err;
   }
-  heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, 8);

-  size_t y_stride;
-  uint8_t *py = heif_image_get_plane2(*image, heif_channel_Y, &y_stride);
-  for (uint32_t row = 0; row < height; row++)
-  {
-    TIFFReadScanline(tif, py, row, 0);
-    py += y_stride;
+  if (bps <= 8) {
+    heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, 8);
+
+    size_t y_stride;
+    uint8_t *py = heif_image_get_plane2(*image, heif_channel_Y, &y_stride);
+    for (uint32_t row = 0; row < height; row++) {
+      TIFFReadScanline(tif, py, row, 0);
+      py += y_stride;
+    }
+  }
+  else {
+    heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, output_bit_depth);
+
+    size_t y_stride;
+    uint8_t *py = heif_image_get_plane2(*image, heif_channel_Y, &y_stride);
+    int bdShift = 16 - output_bit_depth;
+    tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif));
+
+    if (output_bit_depth <= 8) {
+      for (uint32_t row = 0; row < height; row++) {
+        TIFFReadScanline(tif, buf, row, 0);
+        uint16_t* src = static_cast<uint16_t*>(buf);
+        uint8_t* dst = py + row * y_stride;
+        for (uint32_t x = 0; x < width; x++) {
+          dst[x] = static_cast<uint8_t>(src[x] >> 8);
+        }
+      }
+    }
+    else {
+      for (uint32_t row = 0; row < height; row++) {
+        TIFFReadScanline(tif, buf, row, 0);
+        uint16_t* src = static_cast<uint16_t*>(buf);
+        uint16_t* dst = reinterpret_cast<uint16_t*>(py + row * y_stride);
+        for (uint32_t x = 0; x < width; x++) {
+          dst[x] = static_cast<uint16_t>(src[x] >> bdShift);
+        }
+      }
+    }
+    _TIFFfree(buf);
   }
   return heif_error_ok;
 }

-heif_error readPixelInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, heif_image **image)
+heif_error readPixelInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image)
 {
   uint32_t width, height;
   heif_error err = getImageWidthAndHeight(tif, width, height);
   if (err.code != heif_error_Ok) {
     return err;
   }
-  heif_chroma chroma = heif_chroma_interleaved_RGB;
-  if (samplesPerPixel == 4) {
-    chroma = heif_chroma_interleaved_RGBA;
-  }

-  err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image);
-  if (err.code != heif_error_Ok)
-  {
-    return err;
-  }
-  heif_channel channel = heif_channel_interleaved;
-  heif_image_add_plane(*image, channel, (int)width, (int)height, samplesPerPixel * 8);
-
-  size_t y_stride;
-  uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride);
+  uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel;

-  tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif));
-  for (uint32_t row = 0; row < height; row++)
-  {
-    TIFFReadScanline(tif, buf, row, 0);
-    memcpy(py, buf, width * samplesPerPixel);
-    py += y_stride;
+  if (bps <= 8) {
+    heif_chroma chroma = get_heif_chroma(outSpp, 8);
+    err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image);
+    if (err.code != heif_error_Ok) {
+      return err;
+    }
+    heif_channel channel = heif_channel_interleaved;
+    heif_image_add_plane(*image, channel, (int)width, (int)height, outSpp * 8);
+
+    size_t y_stride;
+    uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride);
+    tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif));
+    for (uint32_t row = 0; row < height; row++) {
+      TIFFReadScanline(tif, buf, row, 0);
+      uint8_t* src = static_cast<uint8_t*>(buf);
+      if (outSpp == samplesPerPixel) {
+        memcpy(py, src, width * outSpp);
+      }
+      else {
+        for (uint32_t x = 0; x < width; x++) {
+          memcpy(py + x * outSpp, src + x * samplesPerPixel, outSpp);
+        }
+      }
+      py += y_stride;
+    }
+    _TIFFfree(buf);
+  }
+  else {
+    heif_chroma chroma = get_heif_chroma(outSpp, output_bit_depth);
+    err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image);
+    if (err.code != heif_error_Ok) {
+      return err;
+    }
+    heif_channel channel = heif_channel_interleaved;
+    int planeBitDepth = (output_bit_depth <= 8) ? outSpp * 8 : output_bit_depth;
+    heif_image_add_plane(*image, channel, (int)width, (int)height, planeBitDepth);
+
+    size_t y_stride;
+    uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride);
+    int bdShift = 16 - output_bit_depth;
+    tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif));
+
+    if (output_bit_depth <= 8) {
+      for (uint32_t row = 0; row < height; row++) {
+        TIFFReadScanline(tif, buf, row, 0);
+        uint16_t* src = static_cast<uint16_t*>(buf);
+        uint8_t* dst = py + row * y_stride;
+        if (outSpp == samplesPerPixel) {
+          for (uint32_t x = 0; x < width * outSpp; x++) {
+            dst[x] = static_cast<uint8_t>(src[x] >> 8);
+          }
+        }
+        else {
+          for (uint32_t x = 0; x < width; x++) {
+            for (uint16_t c = 0; c < outSpp; c++) {
+              dst[x * outSpp + c] = static_cast<uint8_t>(src[x * samplesPerPixel + c] >> 8);
+            }
+          }
+        }
+      }
+    }
+    else {
+      for (uint32_t row = 0; row < height; row++) {
+        TIFFReadScanline(tif, buf, row, 0);
+        uint16_t* src = static_cast<uint16_t*>(buf);
+        uint16_t* dst = reinterpret_cast<uint16_t*>(py + row * y_stride);
+        if (outSpp == samplesPerPixel) {
+          for (uint32_t x = 0; x < width * outSpp; x++) {
+            dst[x] = static_cast<uint16_t>(src[x] >> bdShift);
+          }
+        }
+        else {
+          for (uint32_t x = 0; x < width; x++) {
+            for (uint16_t c = 0; c < outSpp; c++) {
+              dst[x * outSpp + c] = static_cast<uint16_t>(src[x * samplesPerPixel + c] >> bdShift);
+            }
+          }
+        }
+      }
+    }
+    _TIFFfree(buf);
   }
-  _TIFFfree(buf);
   return heif_error_ok;
 }

-heif_error readPixelInterleave(TIFF *tif,  uint16_t samplesPerPixel, heif_image **image)
+heif_error readPixelInterleave(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image)
 {
   if (samplesPerPixel == 1) {
-    return readMono(tif, image);
+    return readMono(tif, bps, output_bit_depth, image);
   } else {
-    return readPixelInterleaveRGB(tif, samplesPerPixel, image);
+    return readPixelInterleaveRGB(tif, samplesPerPixel, hasAlpha, bps, output_bit_depth, image);
   }
 }

-heif_error readBandInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, heif_image **image)
+heif_error readBandInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image)
 {
   uint32_t width, height;
   heif_error err = getImageWidthAndHeight(tif, width, height);
   if (err.code != heif_error_Ok) {
     return err;
   }
-  if (samplesPerPixel == 3) {
-    err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, heif_chroma_interleaved_RGB, image);
-  } else {
-    err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, image);
-  }
-  if (err.code != heif_error_Ok) {
-    return err;
-  }
-  heif_channel channel = heif_channel_interleaved;
-  heif_image_add_plane(*image, channel, (int)width, (int)height, samplesPerPixel * 8);

-  size_t y_stride;
-  uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride);
+  uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel;

-  uint8_t *buf = static_cast<uint8_t *>(_TIFFmalloc(TIFFScanlineSize(tif)));
-  for (uint16_t i = 0; i < samplesPerPixel; i++)
-  {
-    uint8_t *dest = py + i;
-    for (uint32_t row = 0; row < height; row++)
-    {
-      TIFFReadScanline(tif, buf, row, i);
-      for (uint32_t x = 0; x < width; x++, dest += samplesPerPixel)
-      {
-        *dest = buf[x];
+  if (bps <= 8) {
+    heif_chroma chroma = get_heif_chroma(outSpp, 8);
+    err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image);
+    if (err.code != heif_error_Ok) {
+      return err;
+    }
+    heif_channel channel = heif_channel_interleaved;
+    heif_image_add_plane(*image, channel, (int)width, (int)height, outSpp * 8);
+
+    size_t y_stride;
+    uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride);
+
+    uint8_t *buf = static_cast<uint8_t *>(_TIFFmalloc(TIFFScanlineSize(tif)));
+    for (uint16_t i = 0; i < outSpp; i++) {
+      uint8_t *dest = py + i;
+      for (uint32_t row = 0; row < height; row++) {
+        TIFFReadScanline(tif, buf, row, i);
+        for (uint32_t x = 0; x < width; x++, dest += outSpp) {
+          *dest = buf[x];
+        }
+        dest += (y_stride - width * outSpp);
       }
-      dest += (y_stride - width * samplesPerPixel);
     }
+    _TIFFfree(buf);
+  }
+  else {
+    heif_chroma chroma = get_heif_chroma(outSpp, output_bit_depth);
+    err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image);
+    if (err.code != heif_error_Ok) {
+      return err;
+    }
+    heif_channel channel = heif_channel_interleaved;
+    int planeBitDepth = (output_bit_depth <= 8) ? outSpp * 8 : output_bit_depth;
+    heif_image_add_plane(*image, channel, (int)width, (int)height, planeBitDepth);
+
+    size_t y_stride;
+    uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride);
+    int bdShift = 16 - output_bit_depth;
+    uint8_t *buf = static_cast<uint8_t *>(_TIFFmalloc(TIFFScanlineSize(tif)));
+
+    if (output_bit_depth <= 8) {
+      for (uint16_t i = 0; i < outSpp; i++) {
+        for (uint32_t row = 0; row < height; row++) {
+          TIFFReadScanline(tif, buf, row, i);
+          uint16_t* src = reinterpret_cast<uint16_t*>(buf);
+          uint8_t* dst = py + row * y_stride + i;
+          for (uint32_t x = 0; x < width; x++) {
+            dst[x * outSpp] = static_cast<uint8_t>(src[x] >> 8);
+          }
+        }
+      }
+    }
+    else {
+      for (uint16_t i = 0; i < outSpp; i++) {
+        for (uint32_t row = 0; row < height; row++) {
+          TIFFReadScanline(tif, buf, row, i);
+          uint16_t* src = reinterpret_cast<uint16_t*>(buf);
+          uint16_t* dst = reinterpret_cast<uint16_t*>(py + row * y_stride);
+          for (uint32_t x = 0; x < width; x++) {
+            dst[x * outSpp + i] = static_cast<uint16_t>(src[x] >> bdShift);
+          }
+        }
+      }
+    }
+    _TIFFfree(buf);
   }
-  _TIFFfree(buf);
   return heif_error_ok;
 }


-heif_error readBandInterleave(TIFF *tif, uint16_t samplesPerPixel, heif_image **image)
+heif_error readBandInterleave(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image)
 {
   if (samplesPerPixel == 1) {
-    return readMono(tif, image);
+    return readMono(tif, bps, output_bit_depth, image);
   } else if (samplesPerPixel == 3) {
-    return readBandInterleaveRGB(tif, samplesPerPixel, image);
+    return readBandInterleaveRGB(tif, samplesPerPixel, hasAlpha, bps, output_bit_depth, image);
   } else if (samplesPerPixel == 4) {
-    return readBandInterleaveRGB(tif,  samplesPerPixel, image);
+    return readBandInterleaveRGB(tif, samplesPerPixel, hasAlpha, bps, output_bit_depth, image);
   } else {
     struct heif_error err = {
       .code = heif_error_Unsupported_feature,
@@ -425,15 +574,15 @@ static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint1
       hasAlpha = (extraTypes[0] == EXTRASAMPLE_ASSOCALPHA || extraTypes[0] == EXTRASAMPLE_UNASSALPHA);
     }
     else {
-      // No EXTRASAMPLES tag with 4 spp — assume RGBA for backward compatibility
-      hasAlpha = true;
+      // No EXTRASAMPLES tag with 4 spp — assume the extra sample is not alpha
+      hasAlpha = false;
     }
   }

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

   uint16_t format;
@@ -448,19 +597,20 @@ 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, heif_image** out_image)
+                                  uint16_t samplesPerPixel, bool hasAlpha,
+                                  uint16_t bps, int output_bit_depth, 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;
+  int effectiveBitDepth = (bps <= 8) ? 8 : output_bit_depth;
+  heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth);
   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);
+  int planeBitDepth = (effectiveBitDepth <= 8) ? outSpp * 8 : effectiveBitDepth;
+  heif_image_add_plane(*out_image, channel, (int)width, (int)height, planeBitDepth);

   size_t out_stride;
   uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride);
@@ -484,16 +634,60 @@ static heif_error readTiledContiguous(TIFF* tif, uint32_t width, uint32_t height
       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);
+      if (bps <= 8) {
+        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 {
+            for (uint32_t x = 0; x < actual_w; x++) {
+              memcpy(dst + x * outSpp, src + x * samplesPerPixel, outSpp);
+            }
+          }
         }
-        else {
-          // Strip extra sample (RGBX -> RGB)
-          for (uint32_t x = 0; x < actual_w; x++) {
-            memcpy(dst + x * outSpp, src + x * samplesPerPixel, outSpp);
+      }
+      else if (output_bit_depth <= 8) {
+        for (uint32_t row = 0; row < actual_h; row++) {
+          uint8_t* dst = out_plane + (ty * tile_height + row) * out_stride + tx * tile_width * outSpp;
+          uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * tile_width * samplesPerPixel * 2);
+          if (outSpp == samplesPerPixel) {
+            for (uint32_t x = 0; x < actual_w * outSpp; x++) {
+              dst[x] = static_cast<uint8_t>(src[x] >> 8);
+            }
+          }
+          else {
+            for (uint32_t x = 0; x < actual_w; x++) {
+              for (uint16_t c = 0; c < outSpp; c++) {
+                dst[x * outSpp + c] = static_cast<uint8_t>(src[x * samplesPerPixel + c] >> 8);
+              }
+            }
+          }
+        }
+      }
+      else {
+        int bdShift = 16 - output_bit_depth;
+        for (uint32_t row = 0; row < actual_h; row++) {
+          uint16_t* dst = reinterpret_cast<uint16_t*>(out_plane + (ty * tile_height + row) * out_stride
+                                                      + tx * tile_width * outSpp * 2);
+          uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * tile_width * samplesPerPixel * 2);
+          if (outSpp == samplesPerPixel) {
+            if (bdShift == 0) {
+              memcpy(dst, src, actual_w * outSpp * 2);
+            }
+            else {
+              for (uint32_t x = 0; x < actual_w * outSpp; x++) {
+                dst[x] = static_cast<uint16_t>(src[x] >> bdShift);
+              }
+            }
+          }
+          else {
+            for (uint32_t x = 0; x < actual_w; x++) {
+              for (uint16_t c = 0; c < outSpp; c++) {
+                dst[x * outSpp + c] = static_cast<uint16_t>(src[x * samplesPerPixel + c] >> bdShift);
+              }
+            }
           }
         }
       }
@@ -506,19 +700,20 @@ 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, heif_image** out_image)
+                                    uint16_t samplesPerPixel, bool hasAlpha,
+                                    uint16_t bps, int output_bit_depth, 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;
+  int effectiveBitDepth = (bps <= 8) ? 8 : output_bit_depth;
+  heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth);
   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);
+  int planeBitDepth = (effectiveBitDepth <= 8) ? outSpp * 8 : effectiveBitDepth;
+  heif_image_add_plane(*out_image, channel, (int)width, (int)height, planeBitDepth);

   size_t out_stride;
   uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride);
@@ -529,7 +724,6 @@ static heif_error readTiledSeparate(TIFF* tif, uint32_t width, uint32_t height,
   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++) {
@@ -544,11 +738,32 @@ static heif_error readTiledSeparate(TIFF* tif, uint32_t width, uint32_t height,
         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];
+        if (bps <= 8) {
+          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];
+            }
+          }
+        }
+        else if (output_bit_depth <= 8) {
+          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;
+            uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * tile_width * 2);
+            for (uint32_t x = 0; x < actual_w; x++) {
+              dst[x * outSpp] = static_cast<uint8_t>(src[x] >> 8);
+            }
+          }
+        }
+        else {
+          int bdShift = 16 - output_bit_depth;
+          for (uint32_t row = 0; row < actual_h; row++) {
+            uint16_t* dst = reinterpret_cast<uint16_t*>(out_plane + (ty * tile_height + row) * out_stride) + tx * tile_width * outSpp + s;
+            uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * tile_width * 2);
+            for (uint32_t x = 0; x < actual_w; x++) {
+              dst[x * outSpp] = static_cast<uint16_t>(src[x] >> bdShift);
+            }
           }
         }
       }
@@ -559,7 +774,7 @@ static heif_error readTiledSeparate(TIFF* tif, uint32_t width, uint32_t height,
 }


-heif_error loadTIFF(const char* filename, InputImage *input_image) {
+heif_error loadTIFF(const char* filename, int output_bit_depth, InputImage *input_image) {
   TIFFSetWarningHandler(suppress_warnings);

   std::unique_ptr<TIFF, void(*)(TIFF*)> tifPtr(TIFFOpen(filename, "r"), [](TIFF* tif) { TIFFClose(tif); });
@@ -574,6 +789,9 @@ heif_error loadTIFF(const char* filename, InputImage *input_image) {
   heif_error err = validateTiffFormat(tif, samplesPerPixel, bps, config, hasAlpha);
   if (err.code != heif_error_Ok) return err;

+  // For 8-bit source, always produce 8-bit output (ignore output_bit_depth).
+  int effectiveOutputBitDepth = (bps <= 8) ? 8 : output_bit_depth;
+
   struct heif_image* image = nullptr;

   if (TIFFIsTiled(tif)) {
@@ -588,10 +806,10 @@ heif_error loadTIFF(const char* filename, InputImage *input_image) {

     switch (config) {
       case PLANARCONFIG_CONTIG:
-        err = readTiledContiguous(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, &image);
+        err = readTiledContiguous(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
         break;
       case PLANARCONFIG_SEPARATE:
-        err = readTiledSeparate(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, &image);
+        err = readTiledSeparate(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
         break;
       default:
         return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"};
@@ -600,10 +818,10 @@ heif_error loadTIFF(const char* filename, InputImage *input_image) {
   else {
     switch (config) {
       case PLANARCONFIG_CONTIG:
-        err = readPixelInterleave(tif, samplesPerPixel, &image);
+        err = readPixelInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
         break;
       case PLANARCONFIG_SEPARATE:
-        err = readBandInterleave(tif, samplesPerPixel, &image);
+        err = readBandInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image);
         break;
       default:
         return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"};
@@ -687,24 +905,24 @@ std::unique_ptr<TiledTiffReader> TiledTiffReader::open(const char* filename, hei
 TiledTiffReader::~TiledTiffReader() = default;


-heif_error TiledTiffReader::readTile(uint32_t tx, uint32_t ty, heif_image** out_image)
+heif_error TiledTiffReader::readTile(uint32_t tx, uint32_t ty, int output_bit_depth, 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);

+  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 = (outSpp == 1) ? heif_chroma_monochrome
-                       : (outSpp == 4) ? heif_chroma_interleaved_RGBA
-                       : heif_chroma_interleaved_RGB;
+  heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth);
   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);
+  int planeBitDepth = (effectiveBitDepth <= 8) ? outSpp * 8 : effectiveBitDepth;
+  heif_image_add_plane(*out_image, channel, (int)actual_w, (int)actual_h, planeBitDepth);

   size_t out_stride;
   uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride);
@@ -721,21 +939,65 @@ heif_error TiledTiffReader::readTile(uint32_t tx, uint32_t ty, heif_image** out_
       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);
+    if (m_bits_per_sample <= 8) {
+      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 {
-        for (uint32_t x = 0; x < actual_w; x++) {
-          memcpy(dst + x * outSpp, src + x * m_samples_per_pixel, outSpp);
+    }
+    else if (output_bit_depth <= 8) {
+      for (uint32_t row = 0; row < actual_h; row++) {
+        uint8_t* dst = out_plane + row * out_stride;
+        uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * m_tile_width * m_samples_per_pixel * 2);
+        if (outSpp == m_samples_per_pixel) {
+          for (uint32_t x = 0; x < actual_w * outSpp; x++) {
+            dst[x] = static_cast<uint8_t>(src[x] >> 8);
+          }
+        }
+        else {
+          for (uint32_t x = 0; x < actual_w; x++) {
+            for (uint16_t c = 0; c < outSpp; c++) {
+              dst[x * outSpp + c] = static_cast<uint8_t>(src[x * m_samples_per_pixel + c] >> 8);
+            }
+          }
+        }
+      }
+    }
+    else {
+      int bdShift = 16 - output_bit_depth;
+      for (uint32_t row = 0; row < actual_h; row++) {
+        uint16_t* dst = reinterpret_cast<uint16_t*>(out_plane + row * out_stride);
+        uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * m_tile_width * m_samples_per_pixel * 2);
+        if (outSpp == m_samples_per_pixel) {
+          if (bdShift == 0) {
+            memcpy(dst, src, actual_w * outSpp * 2);
+          }
+          else {
+            for (uint32_t x = 0; x < actual_w * outSpp; x++) {
+              dst[x] = static_cast<uint16_t>(src[x] >> bdShift);
+            }
+          }
+        }
+        else {
+          for (uint32_t x = 0; x < actual_w; x++) {
+            for (uint16_t c = 0; c < outSpp; c++) {
+              dst[x * outSpp + c] = static_cast<uint16_t>(src[x * m_samples_per_pixel + c] >> bdShift);
+            }
+          }
         }
       }
     }
   }
   else {
-    // PLANARCONFIG_SEPARATE: only read the first outSpp planes
+    // PLANARCONFIG_SEPARATE
     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);
@@ -745,11 +1007,32 @@ heif_error TiledTiffReader::readTile(uint32_t tx, uint32_t ty, heif_image** out_
         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];
+      if (m_bits_per_sample <= 8) {
+        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];
+          }
+        }
+      }
+      else if (output_bit_depth <= 8) {
+        for (uint32_t row = 0; row < actual_h; row++) {
+          uint8_t* dst = out_plane + row * out_stride + s;
+          uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * m_tile_width * 2);
+          for (uint32_t x = 0; x < actual_w; x++) {
+            dst[x * outSpp] = static_cast<uint8_t>(src[x] >> 8);
+          }
+        }
+      }
+      else {
+        int bdShift = 16 - output_bit_depth;
+        for (uint32_t row = 0; row < actual_h; row++) {
+          uint16_t* dst = reinterpret_cast<uint16_t*>(out_plane + row * out_stride) + s;
+          uint16_t* src = reinterpret_cast<uint16_t*>(tile_buf.data() + row * m_tile_width * 2);
+          for (uint32_t x = 0; x < actual_w; x++) {
+            dst[x * outSpp] = static_cast<uint16_t>(src[x] >> bdShift);
+          }
         }
       }
     }
diff --git a/heifio/decoder_tiff.h b/heifio/decoder_tiff.h
index 94cc2bc1..b77aa8e5 100644
--- a/heifio/decoder_tiff.h
+++ b/heifio/decoder_tiff.h
@@ -33,7 +33,7 @@
 #include <cstdint>

 LIBHEIF_API
-heif_error loadTIFF(const char *filename, InputImage *input_image);
+heif_error loadTIFF(const char *filename, int output_bit_depth, InputImage *input_image);

 class LIBHEIF_API TiledTiffReader {
 public:
@@ -50,7 +50,9 @@ public:
   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);
+  uint16_t bitsPerSample() const { return m_bits_per_sample; }
+
+  heif_error readTile(uint32_t tx, uint32_t ty, int output_bit_depth, heif_image** out_image);
   void readExif(InputImage* input_image);

 private:
diff --git a/tests/tiffdecode.cc b/tests/tiffdecode.cc
index d9513f35..d8f77672 100644
--- a/tests/tiffdecode.cc
+++ b/tests/tiffdecode.cc
@@ -56,7 +56,7 @@ void checkMono(InputImage input_image) {
 TEST_CASE("mono8") {
   InputImage input_image;
   std::string path = get_path_for_heifio_test_file("mono.tif");
-  heif_error err = loadTIFF(path.c_str(), &input_image);
+  heif_error err = loadTIFF(path.c_str(), 10, &input_image);
   REQUIRE(err.code == heif_error_Ok);
   checkMono(input_image);
 }
@@ -64,7 +64,7 @@ TEST_CASE("mono8") {
 TEST_CASE("mono8planar") {
   InputImage input_image;
   std::string path = get_path_for_heifio_test_file("mono_planar.tif");
-  heif_error err = loadTIFF(path.c_str(), &input_image);
+  heif_error err = loadTIFF(path.c_str(), 10, &input_image);
   REQUIRE(err.code == heif_error_Ok);
   checkMono(input_image);
 }
@@ -91,7 +91,7 @@ void checkRGB(InputImage input_image) {
 TEST_CASE("rgb") {
   InputImage input_image;
   std::string path = get_path_for_heifio_test_file("rgb.tif");
-  heif_error err = loadTIFF(path.c_str(), &input_image);
+  heif_error err = loadTIFF(path.c_str(), 10, &input_image);
   REQUIRE(err.code == heif_error_Ok);
   checkRGB(input_image);
 }
@@ -99,7 +99,7 @@ TEST_CASE("rgb") {
 TEST_CASE("rgb_planar") {
   InputImage input_image;
   std::string path = get_path_for_heifio_test_file("rgb_planar.tif");
-  heif_error err = loadTIFF(path.c_str(), &input_image);
+  heif_error err = loadTIFF(path.c_str(), 10, &input_image);
   REQUIRE(err.code == heif_error_Ok);
   checkRGB(input_image);
 }
@@ -127,7 +127,7 @@ void checkRGBA(InputImage input_image) {
 TEST_CASE("rgba") {
   InputImage input_image;
   std::string path = get_path_for_heifio_test_file("rgba.tif");
-  heif_error err = loadTIFF(path.c_str(), &input_image);
+  heif_error err = loadTIFF(path.c_str(), 10, &input_image);
   REQUIRE(err.code == heif_error_Ok);
   checkRGBA(input_image);
 }
@@ -135,7 +135,7 @@ TEST_CASE("rgba") {
 TEST_CASE("rgba_planar") {
   InputImage input_image;
   std::string path = get_path_for_heifio_test_file("rgba_planar.tif");
-  heif_error err = loadTIFF(path.c_str(), &input_image);
+  heif_error err = loadTIFF(path.c_str(), 10, &input_image);
   REQUIRE(err.code == heif_error_Ok);
   checkRGBA(input_image);
 }