Skip to content

Commit b9e3a78

Browse files
author
Grok Compression
committed
JP2Grok: support native Grok decompression of vsicurl files
1 parent 18ef941 commit b9e3a78

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
@@ -1525,6 +1526,45 @@ def test_jp2grok_multitile_overview_decode():
15251526
gdal.Unlink(fname)
15261527

15271528

1529+
###############################################################################
1530+
# Test reading a remote JP2 via /vsicurl/ (handled natively by Grok's libcurl
1531+
# backend when available). Uses a real URL, so it is only run when slow
1532+
# tests are enabled.
1533+
1534+
1535+
def test_jp2grok_vsicurl_remote():
1536+
1537+
if not gdaltest.run_slow_tests():
1538+
pytest.skip("GDAL_RUN_SLOW_TESTS not set")
1539+
if "CURL_ENABLED=YES" not in gdal.VersionInfo("BUILD_INFO"):
1540+
pytest.skip("curl not enabled in this GDAL build")
1541+
1542+
url = (
1543+
"/vsicurl/https://www.opengeodata.nrw.de/produkte/geobasis/lusat/"
1544+
"akt/dop/dop_jp2_f10/dop10rgbi_32_280_5653_1_nw_2025.jp2"
1545+
)
1546+
1547+
gdal.VSICurlClearCache()
1548+
try:
1549+
ds = gdal.Open(url)
1550+
if ds is None:
1551+
pytest.skip("remote host unreachable: " + gdal.GetLastErrorMsg())
1552+
assert ds.RasterXSize > 0
1553+
assert ds.RasterYSize > 0
1554+
assert ds.RasterCount >= 1
1555+
# Read a small window from an overview (if any) or from the full-res
1556+
# upper-left corner to exercise the fetch path without pulling
1557+
# too much data.
1558+
band = ds.GetRasterBand(1)
1559+
w = min(64, ds.RasterXSize)
1560+
h = min(64, ds.RasterYSize)
1561+
data = band.ReadRaster(0, 0, w, h, w, h)
1562+
assert data is not None and len(data) > 0
1563+
ds = None
1564+
finally:
1565+
gdal.VSICurlClearCache()
1566+
1567+
15281568
###############################################################################
15291569
# Test driver metadata
15301570

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

0 commit comments

Comments
 (0)