Skip to content

Commit 6968d7f

Browse files
author
Grok Compression
committed
JP2Grok: support native Grok decompression of vsicurl files
1 parent 757e9f7 commit 6968d7f

2 files changed

Lines changed: 466 additions & 9 deletions

File tree

autotest/gdrivers/jp2grok.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import gdaltest
2121
import pytest
22+
import webserver
2223
from test_py_scripts import samples_path
2324

2425
from osgeo import gdal, ogr, osr
@@ -1549,6 +1550,45 @@ def test_jp2grok_multitile_overview_decode():
15491550
gdal.Unlink(fname)
15501551

15511552

1553+
###############################################################################
1554+
# Test reading a remote JP2 via /vsicurl/ (handled natively by Grok's libcurl
1555+
# backend when available). Uses a real URL, so it is only run when slow
1556+
# tests are enabled.
1557+
1558+
1559+
def test_jp2grok_vsicurl_remote():
1560+
1561+
if not gdaltest.run_slow_tests():
1562+
pytest.skip("GDAL_RUN_SLOW_TESTS not set")
1563+
if "CURL_ENABLED=YES" not in gdal.VersionInfo("BUILD_INFO"):
1564+
pytest.skip("curl not enabled in this GDAL build")
1565+
1566+
url = (
1567+
"/vsicurl/https://www.opengeodata.nrw.de/produkte/geobasis/lusat/"
1568+
"akt/dop/dop_jp2_f10/dop10rgbi_32_280_5653_1_nw_2025.jp2"
1569+
)
1570+
1571+
gdal.VSICurlClearCache()
1572+
try:
1573+
ds = gdal.Open(url)
1574+
if ds is None:
1575+
pytest.skip("remote host unreachable: " + gdal.GetLastErrorMsg())
1576+
assert ds.RasterXSize > 0
1577+
assert ds.RasterYSize > 0
1578+
assert ds.RasterCount >= 1
1579+
# Read a small window from an overview (if any) or from the full-res
1580+
# upper-left corner to exercise the fetch path without pulling
1581+
# too much data.
1582+
band = ds.GetRasterBand(1)
1583+
w = min(64, ds.RasterXSize)
1584+
h = min(64, ds.RasterYSize)
1585+
data = band.ReadRaster(0, 0, w, h, w, h)
1586+
assert data is not None and len(data) > 0
1587+
ds = None
1588+
finally:
1589+
gdal.VSICurlClearCache()
1590+
1591+
15521592
###############################################################################
15531593
# Test driver metadata
15541594

@@ -1562,3 +1602,169 @@ def test_jp2grok_driver_metadata():
15621602
assert drv.GetMetadataItem(gdal.DCAP_CREATECOPY) == "YES"
15631603
assert "jp2" in drv.GetMetadataItem(gdal.DMD_EXTENSIONS)
15641604
assert "j2k" in drv.GetMetadataItem(gdal.DMD_EXTENSIONS)
1605+
1606+
1607+
###############################################################################
1608+
# Webserver fixture for HTTP tests
1609+
1610+
1611+
@pytest.fixture(scope="module")
1612+
def server():
1613+
1614+
process, port = webserver.launch(handler=webserver.DispatcherHttpHandler)
1615+
if port == 0:
1616+
pytest.skip("cannot start HTTP server")
1617+
1618+
import collections
1619+
1620+
WebServer = collections.namedtuple("WebServer", "process port")
1621+
1622+
yield WebServer(process, port)
1623+
1624+
gdal.VSICurlClearCache()
1625+
webserver.server_stop(process, port)
1626+
1627+
1628+
###############################################################################
1629+
# Test: blocklisted HTTP settings force VSILFILE fallback.
1630+
#
1631+
# When an unsupported GDAL HTTP config option is set, GrokCanRead() should
1632+
# return false for /vsicurl/ paths, causing the driver to use GDAL's VSILFILE
1633+
# callbacks instead of Grok's native libcurl I/O. The dataset should still
1634+
# open successfully — just via the fallback path.
1635+
1636+
1637+
# Each entry is (config_option, value) that should trigger VSILFILE fallback.
1638+
_BLOCKLIST_CASES = [
1639+
("GDAL_HTTP_AUTH", "NTLM"),
1640+
("GDAL_HTTP_AUTH", "NEGOTIATE"),
1641+
("GDAL_HTTP_SSLCERT", "/path/to/cert.pem"),
1642+
("GDAL_HTTP_SSLKEY", "/path/to/key.pem"),
1643+
("GDAL_HTTP_SSLCERTTYPE", "PEM"),
1644+
("GDAL_HTTP_KEYPASSWD", "secret"),
1645+
("GDAL_HTTP_SSL_VERIFYSTATUS", "YES"),
1646+
("GDAL_CURL_CA_BUNDLE", "/path/to/ca-bundle.crt"),
1647+
("GDAL_HTTP_CAPATH", "/etc/ssl/certs"),
1648+
("GDAL_HTTP_HEADER_FILE", "/tmp/headers.txt"),
1649+
("GDAL_HTTPS_PROXY", "http://proxy:8443"),
1650+
("GDAL_PROXY_AUTH", "NTLM"),
1651+
("GDAL_HTTP_LOW_SPEED_TIME", "30"),
1652+
("GDAL_HTTP_LOW_SPEED_LIMIT", "1024"),
1653+
("GDAL_GSSAPI_DELEGATION", "POLICY"),
1654+
]
1655+
1656+
1657+
@pytest.mark.require_curl()
1658+
@pytest.mark.parametrize(
1659+
"option,value", _BLOCKLIST_CASES, ids=[c[0] for c in _BLOCKLIST_CASES]
1660+
)
1661+
def test_jp2grok_blocklist_fallback(server, option, value, tmp_path):
1662+
"""Blocklisted HTTP settings should trigger VSILFILE fallback while still
1663+
allowing the dataset to open successfully via GDAL's VSI layer."""
1664+
1665+
# GDAL_HTTP_HEADER_FILE requires the file to actually exist, otherwise
1666+
# GDAL logs an error when it tries to read headers from it.
1667+
if option == "GDAL_HTTP_HEADER_FILE":
1668+
header_file = tmp_path / "headers.txt"
1669+
header_file.write_text("X-Test: FallbackValue\n")
1670+
value = str(header_file)
1671+
1672+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1673+
gdal.VSICurlClearCache()
1674+
1675+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1676+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1677+
1678+
with gdal.config_option(option, value):
1679+
with webserver.install_http_handler(handler):
1680+
ds = gdal.Open(url)
1681+
# The dataset should open successfully via the VSILFILE callback
1682+
# path — GDAL's own curl handles the request.
1683+
assert ds is not None
1684+
assert ds.RasterXSize == 100
1685+
assert ds.RasterYSize == 100
1686+
ds = None
1687+
1688+
gdal.VSICurlClearCache()
1689+
1690+
1691+
###############################################################################
1692+
# Test: BASIC and BEARER auth should NOT trigger fallback (Grok handles these).
1693+
1694+
1695+
@pytest.mark.require_curl()
1696+
@pytest.mark.parametrize("auth_scheme", ["BASIC", "BEARER"])
1697+
def test_jp2grok_supported_auth_no_fallback(server, auth_scheme):
1698+
"""BASIC and BEARER auth are handled by Grok natively and should not
1699+
trigger the VSILFILE fallback path."""
1700+
1701+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1702+
gdal.VSICurlClearCache()
1703+
1704+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1705+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1706+
1707+
with gdal.config_option("GDAL_HTTP_AUTH", auth_scheme):
1708+
with webserver.install_http_handler(handler):
1709+
ds = gdal.Open(url)
1710+
assert ds is not None
1711+
assert ds.RasterXSize == 100
1712+
assert ds.RasterYSize == 100
1713+
ds = None
1714+
1715+
gdal.VSICurlClearCache()
1716+
1717+
1718+
###############################################################################
1719+
# Test: GDAL_HTTP_HEADERS with a single custom header should be forwarded
1720+
# to Grok's native I/O (no fallback).
1721+
1722+
1723+
@pytest.mark.require_curl()
1724+
def test_jp2grok_single_custom_header(server):
1725+
"""A single GDAL_HTTP_HEADERS entry should be forwarded to Grok's
1726+
custom_headers[] without triggering VSILFILE fallback."""
1727+
1728+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1729+
gdal.VSICurlClearCache()
1730+
1731+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1732+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1733+
1734+
with gdal.config_option("GDAL_HTTP_HEADERS", "X-Custom: TestValue"):
1735+
with webserver.install_http_handler(handler):
1736+
ds = gdal.Open(url)
1737+
assert ds is not None
1738+
assert ds.RasterXSize == 100
1739+
assert ds.RasterYSize == 100
1740+
ds = None
1741+
1742+
gdal.VSICurlClearCache()
1743+
1744+
1745+
###############################################################################
1746+
# Test: GDAL_HTTP_HEADERS with multiple headers should still work
1747+
# (forwarded to Grok's custom_headers[] array, up to GRK_MAX_CUSTOM_HEADERS).
1748+
1749+
1750+
@pytest.mark.require_curl()
1751+
def test_jp2grok_multiple_custom_headers(server):
1752+
"""Multiple GDAL_HTTP_HEADERS entries should be forwarded to Grok's
1753+
custom_headers[] array."""
1754+
1755+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1756+
gdal.VSICurlClearCache()
1757+
1758+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1759+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1760+
1761+
headers = "X-Custom1: Value1, X-Custom2: Value2, X-Custom3: Value3"
1762+
with gdal.config_option("GDAL_HTTP_HEADERS", headers):
1763+
with webserver.install_http_handler(handler):
1764+
ds = gdal.Open(url)
1765+
assert ds is not None
1766+
assert ds.RasterXSize == 100
1767+
assert ds.RasterYSize == 100
1768+
ds = None
1769+
1770+
gdal.VSICurlClearCache()

0 commit comments

Comments
 (0)