diff --git a/extensions/libtiff/error_handling.h b/extensions/libtiff/error_handling.h index e4be1f8..fbe1621 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 8d0a26b..4cd096a 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 f3b8edc..77c1842 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"-9999\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("