From e48e003ee4da1cf6d5a1568547852180dd789f6d Mon Sep 17 00:00:00 2001 From: shreyaskommuri Date: Thu, 7 May 2026 10:01:38 -0700 Subject: [PATCH 1/2] Suppress spurious libtiff warnings when decoding GeoTIFF files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GeoTIFF embeds geographic metadata using tags that libtiff does not recognise natively (ModelPixelScale 33550, ModelTiepoint 33922, ModelTransformation 34264, GeoKeyDirectory 34735, GeoDoubleParams 34736, GeoAsciiParams 34737, GDAL_METADATA 42112, GDAL_NODATA 42113). libtiff emits "Unknown field with tag X encountered" warnings for each one. The image data is decoded correctly; only the warnings pollute stderr. Install a custom TIFFWarningHandler (SuppressGeoTIFFTagWarnings) in error_handling.h that silently drops warnings for these eight known tags and forwards all other libtiff warnings to stderr unchanged. Call InstallGeoTIFFWarningFilter() — which uses std::call_once so the handler is registered exactly once per process — from libtiffExtensionCreate in libtiff_ext.cpp, which is the natural entry point for the libtiff extension. Add test_decode_tiff_geotiff to test/python/test_decode_tiff.py. The test helper _create_geotiff builds a valid TIFF from scratch using only struct (no third-party dependency), embedding all eight suppressed tags. The test decodes the synthetic file with CPU_ONLY and default backends and asserts correct pixel values. Fixes: NVIDIA/DALI#6114 Signed-off-by: shreyaskommuri --- extensions/libtiff/error_handling.h | 52 ++++++++++++- extensions/libtiff/libtiff_ext.cpp | 1 + test/python/test_decode_tiff.py | 112 +++++++++++++++++++++++++++- 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/extensions/libtiff/error_handling.h b/extensions/libtiff/error_handling.h index e4be1f83..fbe1621f 100644 --- a/extensions/libtiff/error_handling.h +++ b/extensions/libtiff/error_handling.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,12 @@ #pragma once +#include +#include +#include +#include +#include + #define XM_CHECK_NULL(ptr) \ { \ if (!ptr) \ @@ -30,3 +36,47 @@ if (LIBTIFF_CALL_SUCCESS != retcode) \ throw std::runtime_error("libtiff call failed with code " + std::to_string(retcode) + ": " #call); \ } while (0) + +// GeoTIFF/GDAL tag IDs that libtiff does not recognise natively. +// Decoding is correct; the tags carry only geographic metadata. +static constexpr uint32_t kGeoTIFFTags[] = { + 33550, // ModelPixelScaleTag + 33922, // ModelTiepointTag + 34264, // ModelTransformationTag + 34735, // GeoKeyDirectoryTag + 34736, // GeoDoubleParamsTag + 34737, // GeoAsciiParamsTag + 42112, // GDAL_METADATA + 42113, // GDAL_NODATA +}; + +// TIFFWarningHandler that silently drops "Unknown field with tag X" warnings for +// the known GeoTIFF/GDAL tags and forwards all other libtiff warnings to stderr. +inline void SuppressGeoTIFFTagWarnings(const char* module, const char* fmt, va_list ap) +{ + if (strstr(fmt, "Unknown field with tag") != nullptr) { + va_list ap_copy; + va_copy(ap_copy, ap); + unsigned int tag = va_arg(ap_copy, unsigned int); + va_end(ap_copy); + for (auto geotiff_tag : kGeoTIFFTags) { + if (tag == geotiff_tag) + return; + } + } + char buf[1024]; + vsnprintf(buf, sizeof(buf), fmt, ap); + // libtiff permits a null module name in some code paths + if (module) + fprintf(stderr, "%s: %s\n", module, buf); + else + fprintf(stderr, "%s\n", buf); +} + +// Declared inline so the static once_flag is shared across all translation units, +// guaranteeing TIFFSetWarningHandler is called exactly once per process. +inline void InstallGeoTIFFWarningFilter() +{ + static std::once_flag flag; + std::call_once(flag, [] { TIFFSetWarningHandler(SuppressGeoTIFFTagWarnings); }); +} diff --git a/extensions/libtiff/libtiff_ext.cpp b/extensions/libtiff/libtiff_ext.cpp index 8d0a26bc..4cd096a8 100644 --- a/extensions/libtiff/libtiff_ext.cpp +++ b/extensions/libtiff/libtiff_ext.cpp @@ -39,6 +39,7 @@ struct LibtiffImgCodecsExtension try { XM_CHECK_NULL(framework) NVIMGCODEC_LOG_TRACE(framework, "libtiff_ext", "nvimgcodecExtensionCreate"); + InstallGeoTIFFWarningFilter(); XM_CHECK_NULL(extension) *extension = reinterpret_cast(new libtiff::LibtiffImgCodecsExtension(framework)); diff --git a/test/python/test_decode_tiff.py b/test/python/test_decode_tiff.py index f3b8edc4..e954d681 100644 --- a/test/python/test_decode_tiff.py +++ b/test/python/test_decode_tiff.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,8 @@ from __future__ import annotations import os +import struct +import tempfile import numpy as np from nvidia import nvimgcodec import pytest as t @@ -117,3 +119,111 @@ def test_decode_tiff_too_many_planes(): def test_decode_tiff_oom(): assert None == nvimgcodec.Decoder().read( os.path.join(img_dir_path, "tiff/error/oom.tiff")) + + +def _create_geotiff(path, width=4, height=4): + """Build a minimal GeoTIFF from scratch with all eight GeoTIFF/GDAL metadata tags. + + Returns the expected pixel array (HxWx1 uint8) so callers can compare against decoded output. + The file is constructed with struct so no third-party TIFF library is required. + """ + # Image: row-major, values 0..(H*W-1) + image_data = bytes(i % 256 for i in range(width * height)) + + # Extra tag payloads (appended after the IFD) + model_pixel_scale = struct.pack("<3d", 1.0, 1.0, 0.0) # ModelPixelScaleTag (3 doubles) + model_tiepoint = struct.pack("<6d", *([0.0] * 6)) # ModelTiepointTag (6 doubles) + model_transform = struct.pack("<16d", *([0.0] * 16)) # ModelTransformationTag (16 doubles) + geo_key_dir = struct.pack("<4H", 1, 1, 0, 0) # GeoKeyDirectoryTag (4 shorts) + geo_double = struct.pack("\x00" # GDAL_METADATA (ASCII) + gdal_nodata = b"0\x00" # GDAL_NODATA (ASCII) + + # Compute layout offsets + # 0..7: header + # 8..8+len(image_data)-1: pixel data + # ifd_offset: IFD + num_entries = 17 + image_offset = 8 + ifd_offset = image_offset + len(image_data) + entries_offset = ifd_offset + 2 # skip num_entries field + extra_start = entries_offset + num_entries * 12 + 4 # after entries + next-IFD pointer + + off_mps = extra_start + off_mt = off_mps + len(model_pixel_scale) + off_mtx = off_mt + len(model_tiepoint) + off_gkd = off_mtx + len(model_transform) + off_gdp = off_gkd + len(geo_key_dir) + off_gas = off_gdp + len(geo_double) + off_gmd = off_gas + len(geo_ascii) + off_gnd = off_gmd + len(gdal_metadata) + + SHORT, LONG, DOUBLE, ASCII = 3, 4, 12, 2 + + def ifd_entry(tag, ttype, count, val): + return struct.pack(" Date: Thu, 7 May 2026 10:10:49 -0700 Subject: [PATCH 2/2] Fix TIFF spec violation in GeoTIFF test: use 6-byte GDAL_NODATA value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit b"0\x00" is 2 bytes — since count*typesize (2) <= 4, TIFF requires the value to be stored inline in the IFD entry, not as a file offset. The IFD entry was incorrectly treating a file offset as the value. Use b"-9999\x00" (6 bytes) so the offset is spec-valid. Signed-off-by: shreyaskommuri --- test/python/test_decode_tiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/test_decode_tiff.py b/test/python/test_decode_tiff.py index e954d681..77c1842a 100644 --- a/test/python/test_decode_tiff.py +++ b/test/python/test_decode_tiff.py @@ -138,7 +138,7 @@ def _create_geotiff(path, width=4, height=4): geo_double = struct.pack("\x00" # GDAL_METADATA (ASCII) - gdal_nodata = b"0\x00" # GDAL_NODATA (ASCII) + gdal_nodata = b"-9999\x00" # GDAL_NODATA (ASCII) # Compute layout offsets # 0..7: header