Commit 033fde9d for libheif
commit 033fde9d9d546c7faa492a9e5e615274832df36e
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Tue Mar 17 22:52:05 2026 +0100
tiff: implement reading YCbCr (JPEG) images
diff --git a/heifio/decoder_tiff.cc b/heifio/decoder_tiff.cc
index 23d46625..ef193e10 100644
--- a/heifio/decoder_tiff.cc
+++ b/heifio/decoder_tiff.cc
@@ -50,6 +50,16 @@ extern "C" {
static struct heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"};
+// Forward declarations for YCbCr helpers (defined after validateTiffFormat)
+static YCbCrInfo getYCbCrInfo(TIFF* tif);
+static heif_chroma ycbcrChroma(const YCbCrInfo& ycbcr);
+static void deinterleaveYCbCr(const uint8_t* src, uint32_t block_w, uint32_t block_h,
+ uint32_t actual_w, uint32_t actual_h,
+ uint16_t horiz_sub, uint16_t vert_sub,
+ uint8_t* y_plane, size_t y_stride,
+ uint8_t* cb_plane, size_t cb_stride,
+ uint8_t* cr_plane, size_t cr_stride);
+
static bool seekTIFF(TIFF* tif, toff_t offset, int whence) {
TIFFSeekProc seekProc = TIFFGetSeekProc(tif);
if (!seekProc) {
@@ -374,6 +384,61 @@ heif_error readPixelInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, bool hasA
return err;
}
+ // --- YCbCr strip path: TIFFReadScanline doesn't work with packed YCbCr ---
+ YCbCrInfo ycbcr = getYCbCrInfo(tif);
+ if (ycbcr.is_ycbcr) {
+ if (bps != 8) {
+ return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+ "Only 8-bit YCbCr TIFF is supported."};
+ }
+
+ heif_chroma chroma = ycbcrChroma(ycbcr);
+ err = heif_image_create((int)width, (int)height, heif_colorspace_YCbCr, chroma, image);
+ if (err.code != heif_error_Ok) return err;
+
+ heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, 8);
+ uint32_t chroma_w = (width + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub;
+ uint32_t chroma_h = (height + ycbcr.vert_sub - 1) / ycbcr.vert_sub;
+ heif_image_add_plane(*image, heif_channel_Cb, (int)chroma_w, (int)chroma_h, 8);
+ heif_image_add_plane(*image, heif_channel_Cr, (int)chroma_w, (int)chroma_h, 8);
+
+ size_t y_stride, cb_stride, cr_stride;
+ uint8_t* y_plane = heif_image_get_plane2(*image, heif_channel_Y, &y_stride);
+ uint8_t* cb_plane = heif_image_get_plane2(*image, heif_channel_Cb, &cb_stride);
+ uint8_t* cr_plane = heif_image_get_plane2(*image, heif_channel_Cr, &cr_stride);
+
+ uint32_t rows_per_strip = 0;
+ TIFFGetFieldDefaulted(tif, TIFFTAG_ROWSPERSTRIP, &rows_per_strip);
+ if (rows_per_strip == 0) rows_per_strip = height;
+
+ tmsize_t strip_size = TIFFStripSize(tif);
+ std::vector<uint8_t> strip_buf(strip_size);
+
+ uint32_t n_strips = TIFFNumberOfStrips(tif);
+ for (uint32_t s = 0; s < n_strips; s++) {
+ tmsize_t read = TIFFReadEncodedStrip(tif, s, strip_buf.data(), strip_size);
+ if (read < 0) {
+ heif_image_release(*image);
+ *image = nullptr;
+ return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF strip"};
+ }
+
+ uint32_t strip_y = s * rows_per_strip;
+ uint32_t actual_h = std::min(rows_per_strip, height - strip_y);
+ // Packed YCbCr block dimensions must be multiples of subsampling factors
+ uint32_t block_w = ((width + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub) * ycbcr.horiz_sub;
+ uint32_t block_h = ((actual_h + ycbcr.vert_sub - 1) / ycbcr.vert_sub) * ycbcr.vert_sub;
+
+ deinterleaveYCbCr(strip_buf.data(), block_w, block_h, width, actual_h,
+ ycbcr.horiz_sub, ycbcr.vert_sub,
+ y_plane + strip_y * y_stride, y_stride,
+ cb_plane + (strip_y / ycbcr.vert_sub) * cb_stride, cb_stride,
+ cr_plane + (strip_y / ycbcr.vert_sub) * cr_stride, cr_stride);
+ }
+
+ return heif_error_ok;
+ }
+
uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel;
if (bps <= 8) {
@@ -777,12 +842,167 @@ static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint1
}
+static YCbCrInfo getYCbCrInfo(TIFF* tif)
+{
+ YCbCrInfo info;
+ uint16_t photometric;
+ if (TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &photometric) && photometric == PHOTOMETRIC_YCBCR) {
+ info.is_ycbcr = true;
+ TIFFGetFieldDefaulted(tif, TIFFTAG_YCBCRSUBSAMPLING, &info.horiz_sub, &info.vert_sub);
+ }
+ return info;
+}
+
+
+static heif_chroma ycbcrChroma(const YCbCrInfo& ycbcr)
+{
+ if (ycbcr.horiz_sub == 2 && ycbcr.vert_sub == 2) return heif_chroma_420;
+ if (ycbcr.horiz_sub == 2 && ycbcr.vert_sub == 1) return heif_chroma_422;
+ return heif_chroma_444;
+}
+
+
+// Deinterleave libtiff's packed YCbCr format into separate Y/Cb/Cr planes.
+// Packed format: MCUs of (horiz_sub * vert_sub) Y samples + 1 Cb + 1 Cr.
+// block_w/block_h must be multiples of horiz_sub/vert_sub respectively.
+// actual_w/actual_h are the clipped dimensions for edge tiles/strips.
+static void deinterleaveYCbCr(
+ const uint8_t* src,
+ uint32_t block_w, uint32_t block_h,
+ uint32_t actual_w, uint32_t actual_h,
+ uint16_t horiz_sub, uint16_t vert_sub,
+ uint8_t* y_plane, size_t y_stride,
+ uint8_t* cb_plane, size_t cb_stride,
+ uint8_t* cr_plane, size_t cr_stride)
+{
+ uint32_t mcus_h = block_w / horiz_sub;
+ uint32_t mcu_rows = block_h / vert_sub;
+ uint32_t mcu_size = horiz_sub * vert_sub + 2;
+
+ uint32_t chroma_w = (actual_w + horiz_sub - 1) / horiz_sub;
+ uint32_t chroma_h = (actual_h + vert_sub - 1) / vert_sub;
+
+ for (uint32_t mcu_y = 0; mcu_y < mcu_rows; mcu_y++) {
+ for (uint32_t mcu_x = 0; mcu_x < mcus_h; mcu_x++) {
+ const uint8_t* mcu = src + (mcu_y * mcus_h + mcu_x) * mcu_size;
+
+ // Scatter Y samples
+ for (uint32_t vy = 0; vy < vert_sub; vy++) {
+ uint32_t py = mcu_y * vert_sub + vy;
+ if (py >= actual_h) break;
+ for (uint32_t hx = 0; hx < horiz_sub; hx++) {
+ uint32_t px = mcu_x * horiz_sub + hx;
+ if (px >= actual_w) break;
+ y_plane[py * y_stride + px] = mcu[vy * horiz_sub + hx];
+ }
+ }
+
+ // Scatter Cb/Cr
+ if (mcu_x < chroma_w && mcu_y < chroma_h) {
+ cb_plane[mcu_y * cb_stride + mcu_x] = mcu[horiz_sub * vert_sub];
+ cr_plane[mcu_y * cr_stride + mcu_x] = mcu[horiz_sub * vert_sub + 1];
+ }
+ }
+ }
+}
+
+
+// Create a YCbCr heif_image from a single packed YCbCr tile/block.
+static heif_error readYCbCrBlock(
+ const uint8_t* tile_buf,
+ uint32_t block_w, uint32_t block_h,
+ uint32_t actual_w, uint32_t actual_h,
+ const YCbCrInfo& ycbcr,
+ heif_image** out_image)
+{
+ heif_chroma chroma = ycbcrChroma(ycbcr);
+
+ heif_error err = heif_image_create((int)actual_w, (int)actual_h, heif_colorspace_YCbCr, chroma, out_image);
+ if (err.code != heif_error_Ok) return err;
+
+ heif_image_add_plane(*out_image, heif_channel_Y, (int)actual_w, (int)actual_h, 8);
+ uint32_t chroma_w = (actual_w + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub;
+ uint32_t chroma_h = (actual_h + ycbcr.vert_sub - 1) / ycbcr.vert_sub;
+ heif_image_add_plane(*out_image, heif_channel_Cb, (int)chroma_w, (int)chroma_h, 8);
+ heif_image_add_plane(*out_image, heif_channel_Cr, (int)chroma_w, (int)chroma_h, 8);
+
+ size_t y_stride, cb_stride, cr_stride;
+ uint8_t* y_plane = heif_image_get_plane2(*out_image, heif_channel_Y, &y_stride);
+ uint8_t* cb_plane = heif_image_get_plane2(*out_image, heif_channel_Cb, &cb_stride);
+ uint8_t* cr_plane = heif_image_get_plane2(*out_image, heif_channel_Cr, &cr_stride);
+
+ deinterleaveYCbCr(tile_buf, block_w, block_h, actual_w, actual_h,
+ ycbcr.horiz_sub, ycbcr.vert_sub,
+ y_plane, y_stride, cb_plane, cb_stride, cr_plane, cr_stride);
+
+ 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,
uint16_t bps, int output_bit_depth,
uint16_t sampleFormat, heif_image** out_image)
{
+ // --- YCbCr path: deinterleave packed MCU data into planar Y/Cb/Cr ---
+ YCbCrInfo ycbcr = getYCbCrInfo(tif);
+ if (ycbcr.is_ycbcr) {
+ if (bps != 8) {
+ return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+ "Only 8-bit YCbCr TIFF is supported."};
+ }
+
+ heif_chroma chroma = ycbcrChroma(ycbcr);
+ heif_error err = heif_image_create((int)width, (int)height, heif_colorspace_YCbCr, chroma, out_image);
+ if (err.code != heif_error_Ok) return err;
+
+ heif_image_add_plane(*out_image, heif_channel_Y, (int)width, (int)height, 8);
+ uint32_t chroma_w = (width + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub;
+ uint32_t chroma_h = (height + ycbcr.vert_sub - 1) / ycbcr.vert_sub;
+ heif_image_add_plane(*out_image, heif_channel_Cb, (int)chroma_w, (int)chroma_h, 8);
+ heif_image_add_plane(*out_image, heif_channel_Cr, (int)chroma_w, (int)chroma_h, 8);
+
+ size_t y_stride, cb_stride, cr_stride;
+ uint8_t* y_plane = heif_image_get_plane2(*out_image, heif_channel_Y, &y_stride);
+ uint8_t* cb_plane = heif_image_get_plane2(*out_image, heif_channel_Cb, &cb_stride);
+ uint8_t* cr_plane = heif_image_get_plane2(*out_image, heif_channel_Cr, &cr_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);
+
+ uint32_t y_offset = ty * tile_height;
+ uint32_t x_offset = tx * tile_width;
+ uint32_t chroma_x_offset = x_offset / ycbcr.horiz_sub;
+ uint32_t chroma_y_offset = y_offset / ycbcr.vert_sub;
+
+ deinterleaveYCbCr(tile_buf.data(), tile_width, tile_height, actual_w, actual_h,
+ ycbcr.horiz_sub, ycbcr.vert_sub,
+ y_plane + y_offset * y_stride + x_offset, y_stride,
+ cb_plane + chroma_y_offset * cb_stride + chroma_x_offset, cb_stride,
+ cr_plane + chroma_y_offset * cr_stride + chroma_x_offset, cr_stride);
+ }
+ }
+
+ return heif_error_ok;
+ }
+
bool isFloat = (sampleFormat == SAMPLEFORMAT_IEEEFP);
if (isFloat) {
@@ -1112,6 +1332,12 @@ heif_error loadTIFF(const char* filename, int output_bit_depth, InputImage *inpu
heif_error err = validateTiffFormat(tif, samplesPerPixel, bps, config, hasAlpha, sampleFormat);
if (err.code != heif_error_Ok) return err;
+ // For PLANARCONFIG_SEPARATE + YCbCr, tell libtiff to convert to RGB on the fly
+ YCbCrInfo ycbcr = getYCbCrInfo(tif);
+ if (ycbcr.is_ycbcr && config == PLANARCONFIG_SEPARATE) {
+ TIFFSetField(tif, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
+ }
+
bool isFloat = (sampleFormat == SAMPLEFORMAT_IEEEFP);
bool isSignedInt = (sampleFormat == SAMPLEFORMAT_INT);
@@ -1227,6 +1453,12 @@ std::unique_ptr<TiledTiffReader> TiledTiffReader::open(const char* filename, hei
}
reader->m_bits_per_sample = bps;
+ reader->m_ycbcr = getYCbCrInfo(tif);
+ if (reader->m_ycbcr.is_ycbcr && reader->m_planar_config == PLANARCONFIG_SEPARATE) {
+ TIFFSetField(tif, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
+ reader->m_ycbcr.is_ycbcr = false; // data comes out as RGB now
+ }
+
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"};
@@ -1315,6 +1547,12 @@ bool TiledTiffReader::setDirectory(uint32_t dir_index)
}
m_bits_per_sample = bps;
+ m_ycbcr = getYCbCrInfo(tif);
+ if (m_ycbcr.is_ycbcr && m_planar_config == PLANARCONFIG_SEPARATE) {
+ TIFFSetField(tif, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
+ m_ycbcr.is_ycbcr = false;
+ }
+
m_n_columns = (m_image_width + m_tile_width - 1) / m_tile_width;
m_n_rows = (m_image_height + m_tile_height - 1) / m_tile_height;
@@ -1329,6 +1567,25 @@ 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);
+ // --- YCbCr path (PLANARCONFIG_CONTIG only; SEPARATE was converted to RGB in open/setDirectory) ---
+ if (m_ycbcr.is_ycbcr && m_planar_config == PLANARCONFIG_CONTIG) {
+ if (m_bits_per_sample != 8) {
+ return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+ "Only 8-bit YCbCr TIFF is supported."};
+ }
+
+ 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) {
+ return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"};
+ }
+
+ return readYCbCrBlock(tile_buf.data(), m_tile_width, m_tile_height, actual_w, actual_h, m_ycbcr, out_image);
+ }
+
if (m_sample_format == SAMPLEFORMAT_IEEEFP) {
#if WITH_UNCOMPRESSED_CODEC
heif_error err = heif_image_create((int)actual_w, (int)actual_h, heif_colorspace_nonvisual, heif_chroma_undefined, out_image);
diff --git a/heifio/decoder_tiff.h b/heifio/decoder_tiff.h
index ab3676b2..1e34a168 100644
--- a/heifio/decoder_tiff.h
+++ b/heifio/decoder_tiff.h
@@ -33,6 +33,12 @@
#include <cstdint>
#include <vector>
+struct YCbCrInfo {
+ bool is_ycbcr = false;
+ uint16_t horiz_sub = 1; // horizontal subsampling factor
+ uint16_t vert_sub = 1; // vertical subsampling factor
+};
+
LIBHEIF_API
heif_error loadTIFF(const char *filename, int output_bit_depth, InputImage *input_image);
@@ -83,6 +89,7 @@ private:
uint16_t m_planar_config = 0;
uint16_t m_sample_format = 1; // SAMPLEFORMAT_UINT
bool m_has_alpha = false;
+ YCbCrInfo m_ycbcr;
std::vector<OverviewInfo> m_overviews;
};