Skip to content

Commit

Permalink
Merge pull request #11475 from rouault/vat_dbf
Browse files Browse the repository at this point in the history
Tiff: add support to read RAT stored in ArcGIS style .tif.vat.dbf auxiliary file
  • Loading branch information
rouault authored Dec 12, 2024
2 parents a047ded + 475edfc commit 87f83c8
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 4 deletions.
Binary file added autotest/gcore/data/gtiff/testrat.tif
Binary file not shown.
Binary file added autotest/gcore/data/gtiff/testrat.tif.vat.dbf
Binary file not shown.
105 changes: 105 additions & 0 deletions autotest/gcore/tiff_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -5361,3 +5361,108 @@ def test_tiff_read_ovr_dimap_pleiades(tmp_path):
ds = None
# Check that cleaning overviews did not suppress the DIMAP XML file
assert os.path.exists(tmp_path / "bundle" / "DIM_foo.XML")


###############################################################################
# Test reading ArcGIS .tif.vat.dbf auxiliary file


@pytest.mark.parametrize("GDAL_DISABLE_READDIR_ON_OPEN", ["NO", "YES", "EMPTY_DIR"])
def test_tiff_read_vat_dbf(GDAL_DISABLE_READDIR_ON_OPEN):

with gdal.config_option(
"GDAL_DISABLE_READDIR_ON_OPEN", GDAL_DISABLE_READDIR_ON_OPEN
):
with gdal.Open("data/gtiff/testrat.tif") as ds:
band = ds.GetRasterBand(1)
rat = band.GetDefaultRAT()

if GDAL_DISABLE_READDIR_ON_OPEN == "EMPTY_DIR":
assert rat is None
return

assert rat
assert rat.GetColumnCount() == 9
assert rat.GetRowCount() == 2
assert [rat.GetNameOfCol(i) for i in range(9)] == [
"VALUE",
"COUNT",
"CLASS",
"Red",
"Green",
"Blue",
"OtherInt",
"OtherReal",
"OtherStr",
]
assert [rat.GetUsageOfCol(i) for i in range(9)] == [
gdal.GFU_MinMax,
gdal.GFU_PixelCount,
gdal.GFU_Name,
gdal.GFU_Red,
gdal.GFU_Green,
gdal.GFU_Blue,
gdal.GFU_Generic,
gdal.GFU_Generic,
gdal.GFU_Generic,
]
assert [rat.GetTypeOfCol(i) for i in range(9)] == [
gdal.GFT_Integer,
gdal.GFT_Integer,
gdal.GFT_String,
gdal.GFT_Integer,
gdal.GFT_Integer,
gdal.GFT_Integer,
gdal.GFT_Integer,
gdal.GFT_Real,
gdal.GFT_String,
]
assert rat.GetValueAsInt(0, 0) == 1
assert rat.GetValueAsInt(0, 1) == 10
assert rat.GetValueAsString(0, 2) == "my class"
assert rat.GetValueAsInt(0, 3) == 26
assert rat.GetValueAsInt(0, 4) == 51
assert rat.GetValueAsInt(0, 5) == 128
assert rat.GetValueAsInt(0, 6) == 2
assert rat.GetValueAsDouble(0, 7) == 2.5
assert rat.GetValueAsString(0, 8) == "foo"

assert rat.GetValueAsInt(1, 0) == 2
assert rat.GetValueAsString(1, 2) == "my class2"
assert rat.GetValueAsString(1, 8) == "foo2"

rat = band.GetDefaultRAT()
assert rat
assert rat.GetColumnCount() == 9


###############################################################################
# Test reading absent ArcGIS .tif.vat.dbf auxiliary file


@pytest.mark.parametrize("GDAL_DISABLE_READDIR_ON_OPEN", ["NO", "YES", "EMPTY_DIR"])
def test_tiff_read_no_vat_dbf(GDAL_DISABLE_READDIR_ON_OPEN):

with gdal.config_option(
"GDAL_DISABLE_READDIR_ON_OPEN", GDAL_DISABLE_READDIR_ON_OPEN
):
with gdal.Open("data/byte.tif") as ds:
band = ds.GetRasterBand(1)
assert band.GetDefaultRAT() is None


###############################################################################
# Test reading corrupted ArcGIS .tif.vat.dbf auxiliary file


def test_tiff_read_corrupted_vat_dbf(tmp_vsimem):

filename = str(tmp_vsimem / "test.tif")
gdal.GetDriverByName("GTiff").Create(filename, 1, 1)
vat_dbf_filename = filename + ".vat.dbf"
gdal.FileFromMemBuffer(vat_dbf_filename, "")

with gdal.Open(filename) as ds:
band = ds.GetRasterBand(1)
with pytest.raises(Exception):
band.GetDefaultRAT()
9 changes: 8 additions & 1 deletion doc/source/drivers/raster/gtiff.rst
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ GDALGeoTIFF. Note that all bands must use the same nodata value. When
BASELINE or GeoTIFF profile are used, the nodata value is stored into a
PAM .aux.xml file.

Raster Attribute Table
----------------------

Starting with GDAL 3.11, Raster attribute tables stored in auxiliary
.tif.vat.dbf files, as written by ArcGIS, can be read as GDAL Raster Attribute
Table.

Sparse files
------------

Expand Down Expand Up @@ -487,7 +494,7 @@ This driver supports the following creation options:
* ``JXL`` is for JPEG-XL, and is only available when using internal libtiff and building GDAL against
https://github.com/libjxl/libjxl . It is recommended to use JXL compression with the ``TILED=YES`` creation
option and block size of 256x256, 512x512, or 1024x1024 pixels. Supported data types are ``Byte``,
``UInt16`` and ``Float32`` only. For GDAL < 3.6.0, JXL compression may only be used alongside
``UInt16`` and ``Float32`` only. For GDAL < 3.6.0, JXL compression may only be used alongside
``INTERLEAVE=PIXEL`` (the default) on datasets with 4 bands or less.

* ``NONE`` is the default.
Expand Down
2 changes: 2 additions & 0 deletions frmts/gtiff/gtiffrasterband.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#define GTIFFRASTERBAND_H_INCLUDED

#include "gdal_pam.h"
#include "gdal_rat.h"

#include "gtiff.h"

Expand All @@ -41,6 +42,7 @@ class GTiffRasterBand CPL_NON_FINAL : public GDALPamRasterBand
GDALColorInterp m_eBandInterp = GCI_Undefined;
std::set<GTiffRasterBand **> m_aSetPSelf{};
bool m_bHaveOffsetScale = false;
std::unique_ptr<GDALRasterAttributeTable> m_poRAT{};

int DirectIO(GDALRWFlag eRWFlag, int nXOff, int nYOff, int nXSize,
int nYSize, void *pData, int nBufXSize, int nBufYSize,
Expand Down
33 changes: 32 additions & 1 deletion frmts/gtiff/gtiffrasterband_read.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,39 @@
GDALRasterAttributeTable *GTiffRasterBand::GetDefaultRAT()

{
if (m_poRAT)
return m_poRAT.get();

m_poGDS->LoadGeoreferencingAndPamIfNeeded();
return GDALPamRasterBand::GetDefaultRAT();
auto poRAT = GDALPamRasterBand::GetDefaultRAT();
if (poRAT)
return poRAT;

if (!GDALCanFileAcceptSidecarFile(m_poGDS->m_pszFilename))
return nullptr;
const std::string osVATDBF =
std::string(m_poGDS->m_pszFilename) + ".vat.dbf";
CSLConstList papszSiblingFiles = m_poGDS->GetSiblingFiles();
if (papszSiblingFiles &&
// cppcheck-suppress knownConditionTrueFalse
GDALCanReliablyUseSiblingFileList(osVATDBF.c_str()))
{
int iSibling =
CSLFindString(papszSiblingFiles, CPLGetFilename(osVATDBF.c_str()));
if (iSibling >= 0)
{
CPLString osFilename = m_poGDS->m_pszFilename;
osFilename.resize(strlen(m_poGDS->m_pszFilename) -
strlen(CPLGetFilename(m_poGDS->m_pszFilename)));
osFilename += papszSiblingFiles[iSibling];
m_poRAT = GDALLoadVATDBF(osFilename.c_str());
}
return m_poRAT.get();
}
VSIStatBufL sStatBuf;
if (VSIStatL(osVATDBF.c_str(), &sStatBuf) == 0)
m_poRAT = GDALLoadVATDBF(osVATDBF.c_str());
return m_poRAT.get();
}

/************************************************************************/
Expand Down
1 change: 1 addition & 0 deletions gcore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ add_library(
gdaljp2box.cpp
gdalmultidomainmetadata.cpp
gdal_rat.cpp
gdal_rat_vat_dbf.cpp
gdalpamproxydb.cpp
gdalallvalidmaskband.cpp
gdalnodatamaskband.cpp
Expand Down
50 changes: 48 additions & 2 deletions gcore/gdal_rat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -648,11 +648,57 @@ CPLXMLNode *GDALRasterAttributeTable::Serialize() const

snprintf(szValue, sizeof(szValue), "%d",
static_cast<int>(GetTypeOfCol(iCol)));
CPLCreateXMLElementAndValue(psCol, "Type", szValue);
CPLXMLNode *psType =
CPLCreateXMLElementAndValue(psCol, "Type", szValue);
const char *pszTypeStr = "String";
switch (GetTypeOfCol(iCol))
{
case GFT_Integer:
pszTypeStr = "Integer";
break;
case GFT_Real:
pszTypeStr = "Real";
break;
case GFT_String:
break;
}
CPLAddXMLAttributeAndValue(psType, "typeAsString", pszTypeStr);

snprintf(szValue, sizeof(szValue), "%d",
static_cast<int>(GetUsageOfCol(iCol)));
CPLCreateXMLElementAndValue(psCol, "Usage", szValue);
CPLXMLNode *psUsage =
CPLCreateXMLElementAndValue(psCol, "Usage", szValue);
const char *pszUsageStr = "";

#define USAGE_STR(x) \
case GFU_##x: \
pszUsageStr = #x; \
break
switch (GetUsageOfCol(iCol))
{
USAGE_STR(Generic);
USAGE_STR(PixelCount);
USAGE_STR(Name);
USAGE_STR(Min);
USAGE_STR(Max);
USAGE_STR(MinMax);
USAGE_STR(Red);
USAGE_STR(Green);
USAGE_STR(Blue);
USAGE_STR(Alpha);
USAGE_STR(RedMin);
USAGE_STR(GreenMin);
USAGE_STR(BlueMin);
USAGE_STR(AlphaMin);
USAGE_STR(RedMax);
USAGE_STR(GreenMax);
USAGE_STR(BlueMax);
USAGE_STR(AlphaMax);
case GFU_MaxCount:
break;
}
#undef USAGE_STR
CPLAddXMLAttributeAndValue(psUsage, "usageAsString", pszUsageStr);
}

/* -------------------------------------------------------------------- */
Expand Down
3 changes: 3 additions & 0 deletions gcore/gdal_rat.h
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,7 @@ class CPL_DLL GDALDefaultRasterAttributeTable : public GDALRasterAttributeTable
void RemoveStatistics() override;
};

std::unique_ptr<GDALRasterAttributeTable>
CPL_DLL GDALLoadVATDBF(const char *pszFilename);

#endif /* ndef GDAL_RAT_H_INCLUDED */
Loading

0 comments on commit 87f83c8

Please sign in to comment.