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
7 changes: 7 additions & 0 deletions xrspatial/geotiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,13 @@ def read_geotiff_dask(source: str, *,
finally:
_src.close()
http_meta = (http_header, http_ifd)
if http_ifd.orientation != 1:
raise ValueError(
f"Orientation tag (274) is {http_ifd.orientation}; "
f"dask-chunked reads (chunks=...) are not supported for "
f"non-default orientation on remote GeoTIFF sources. Read "
f"the full array first, then slice/chunk it."
)
# Wrap the parsed metadata in a single dask Delayed so every
# window task takes it as a graph input, not a Python closure.
# Without this, the (TIFFHeader, IFD) pair -- which can carry
Expand Down
81 changes: 81 additions & 0 deletions xrspatial/geotiff/tests/test_http_dask_orientation_1794.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Remote dask reads must not bypass TIFF Orientation handling (#1794)."""
from __future__ import annotations

import http.server
import socketserver
import threading

import numpy as np
import pytest

from xrspatial.geotiff import open_geotiff

tifffile = pytest.importorskip("tifffile")


def _write_with_orientation(path, arr, orientation):
tifffile.imwrite(
str(path),
arr,
extratags=[(274, 'H', 1, orientation, True)],
)


class _RangeHandler(http.server.BaseHTTPRequestHandler):
payload: bytes = b''

def do_GET(self): # noqa: N802
rng = self.headers.get('Range')
if rng and rng.startswith('bytes='):
spec = rng[len('bytes='):]
start_s, _, end_s = spec.partition('-')
start = int(start_s)
end = int(end_s) if end_s else len(self.payload) - 1
chunk = self.payload[start:end + 1]
self.send_response(206)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header(
'Content-Range',
f'bytes {start}-{start + len(chunk) - 1}/{len(self.payload)}',
)
self.send_header('Content-Length', str(len(chunk)))
self.end_headers()
self.wfile.write(chunk)
return
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Length', str(len(self.payload)))
self.end_headers()
self.wfile.write(self.payload)

def log_message(self, *_args, **_kwargs):
pass


def _serve(payload: bytes):
handler_cls = type(
'RangeHandler1794', (_RangeHandler,), {'payload': payload}
)
httpd = socketserver.TCPServer(('127.0.0.1', 0), handler_cls)
port = httpd.server_address[1]
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
return httpd, port


def test_http_dask_read_rejects_non_default_orientation(tmp_path, monkeypatch):
monkeypatch.setenv('XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS', '1')
arr = np.arange(64, dtype=np.uint8).reshape(8, 8)
path = tmp_path / "tmp_1794_orientation_2.tif"
_write_with_orientation(path, arr, 2)

payload = path.read_bytes()
httpd, port = _serve(payload)
try:
url = f'http://127.0.0.1:{port}/tmp_1794_orientation_2.tif'
with pytest.raises(ValueError, match="Orientation tag"):
open_geotiff(url, chunks=4)
finally:
httpd.shutdown()
httpd.server_close()

Loading