Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions xrspatial/geotiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,8 @@ def to_geotiff(data: xr.DataArray | np.ndarray,
tile_size=tile_size,
predictor=predictor,
bigtiff=bigtiff,
max_z_error=max_z_error)
max_z_error=max_z_error,
photometric=photometric)
return

# Auto-detect GPU data and dispatch to write_geotiff_gpu. ``gpu is
Expand Down Expand Up @@ -1678,7 +1679,8 @@ def _write_single_tile(chunk_data, path, geo_transform, epsg, wkt,
y_resolution=None,
resolution_unit=None,
gdal_metadata_xml=None,
extra_tags=None):
extra_tags=None,
photometric: str | int = 'auto'):
"""Write a single tile GeoTIFF. Used by _write_vrt_tiled.

Forwards the same rich-tag set that ``to_geotiff`` passes through to
Expand Down Expand Up @@ -1730,13 +1732,15 @@ def _write_single_tile(chunk_data, path, geo_transform, epsg, wkt,
gdal_metadata_xml=gdal_metadata_xml,
extra_tags=extra_tags,
bigtiff=bigtiff,
max_z_error=max_z_error)
max_z_error=max_z_error,
photometric=photometric)


def _write_vrt_tiled(data, vrt_path, *, crs=None, nodata=None,
compression='zstd', compression_level=None,
tile_size=256, predictor: bool | int = False,
bigtiff=None, max_z_error: float = 0.0):
bigtiff=None, max_z_error: float = 0.0,
photometric: str | int = 'auto'):
"""Write a DataArray as a directory of tiled GeoTIFFs with a VRT index.

This enables streaming dask arrays to disk without materializing the
Expand Down Expand Up @@ -1903,7 +1907,8 @@ def _write_vrt_tiled(data, vrt_path, *, crs=None, nodata=None,
y_resolution=y_res,
resolution_unit=res_unit,
gdal_metadata_xml=gdal_meta_xml,
extra_tags=extra_tags_list)
extra_tags=extra_tags_list,
photometric=photometric)
delayed_tasks.append(task)
else:
# Numpy: slice and write directly
Expand All @@ -1918,7 +1923,8 @@ def _write_vrt_tiled(data, vrt_path, *, crs=None, nodata=None,
y_resolution=y_res,
resolution_unit=res_unit,
gdal_metadata_xml=gdal_meta_xml,
extra_tags=extra_tags_list)
extra_tags=extra_tags_list,
photometric=photometric)

col_offset += chunk_w
row_offset += chunk_h
Expand Down
75 changes: 75 additions & 0 deletions xrspatial/geotiff/tests/test_vrt_writer_photometric_1861.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Regression test for issue #1861: ``photometric`` dropped by VRT writer.

``to_geotiff(data, '.vrt', photometric=...)`` accepted the kwarg at the
public boundary but ``_write_vrt_tiled`` did not take ``photometric`` and
``_write_single_tile`` did not forward it to ``write(...)``. Per-tile
TIFFs were always tagged with the default Photometric=MinIsBlack (1) no
matter what the caller requested.

This test pins the fix: the kwarg now threads through to every per-tile
``write`` call so the on-disk Photometric tag matches the request.
"""
from __future__ import annotations

import glob
import os

import numpy as np
import xarray as xr

from xrspatial.geotiff import to_geotiff
from xrspatial.geotiff._header import TAG_PHOTOMETRIC, parse_header, parse_ifd


def _read_primary_ifd(path: str):
with open(path, 'rb') as f:
raw = f.read()
hdr = parse_header(raw[:16])
return parse_ifd(raw, hdr.first_ifd_offset, hdr)


def _tile_paths(vrt_path: str):
stem = os.path.splitext(os.path.basename(vrt_path))[0]
tiles_dir = os.path.join(
os.path.dirname(os.path.abspath(vrt_path)),
stem + '_tiles',
)
return sorted(glob.glob(os.path.join(tiles_dir, 'tile_*.tif')))


def test_vrt_writer_forwards_photometric_miniswhite_1861(tmp_path):
"""photometric='miniswhite' must tag every per-tile TIFF with
PhotometricInterpretation = 0 (MinIsWhite)."""
arr = np.zeros((48, 48), dtype=np.uint8)
da = xr.DataArray(arr, dims=('y', 'x'))
vrt_path = str(tmp_path / 'miniswhite_1861.vrt')

to_geotiff(da, vrt_path, photometric='miniswhite', tile_size=16)

tiles = _tile_paths(vrt_path)
assert tiles, 'expected at least one per-tile TIFF under _tiles/'
for tile in tiles:
ifd = _read_primary_ifd(tile)
assert ifd.get_value(TAG_PHOTOMETRIC) == 0, (
f'tile {tile} has Photometric '
f'{ifd.get_value(TAG_PHOTOMETRIC)}, expected 0 (MinIsWhite)'
)


def test_vrt_writer_default_photometric_minisblack_1861(tmp_path):
"""Control: default photometric='auto' keeps per-tile TIFFs at
PhotometricInterpretation = 1 (MinIsBlack)."""
arr = np.zeros((48, 48), dtype=np.uint8)
da = xr.DataArray(arr, dims=('y', 'x'))
vrt_path = str(tmp_path / 'default_auto_1861.vrt')

to_geotiff(da, vrt_path, tile_size=16)

tiles = _tile_paths(vrt_path)
assert tiles, 'expected at least one per-tile TIFF under _tiles/'
for tile in tiles:
ifd = _read_primary_ifd(tile)
assert ifd.get_value(TAG_PHOTOMETRIC) == 1, (
f'tile {tile} has Photometric '
f'{ifd.get_value(TAG_PHOTOMETRIC)}, expected 1 (MinIsBlack)'
)
Loading