Skip to content

Commit 346582f

Browse files
author
Grok Compression
committed
JP2Grok: support native Grok decompression of vsicurl files
1 parent 4287fac commit 346582f

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
@@ -1416,6 +1417,45 @@ def test_jp2grok_overview_decode():
14161417
gdal.Unlink(fname)
14171418

14181419

1420+
###############################################################################
1421+
# Test reading a remote JP2 via /vsicurl/ (handled natively by Grok's libcurl
1422+
# backend when available). Uses a real URL, so it is only run when slow
1423+
# tests are enabled.
1424+
1425+
1426+
def test_jp2grok_vsicurl_remote():
1427+
1428+
if not gdaltest.run_slow_tests():
1429+
pytest.skip("GDAL_RUN_SLOW_TESTS not set")
1430+
if "CURL_ENABLED=YES" not in gdal.VersionInfo("BUILD_INFO"):
1431+
pytest.skip("curl not enabled in this GDAL build")
1432+
1433+
url = (
1434+
"/vsicurl/https://www.opengeodata.nrw.de/produkte/geobasis/lusat/"
1435+
"akt/dop/dop_jp2_f10/dop10rgbi_32_280_5653_1_nw_2025.jp2"
1436+
)
1437+
1438+
gdal.VSICurlClearCache()
1439+
try:
1440+
ds = gdal.Open(url)
1441+
if ds is None:
1442+
pytest.skip("remote host unreachable: " + gdal.GetLastErrorMsg())
1443+
assert ds.RasterXSize > 0
1444+
assert ds.RasterYSize > 0
1445+
assert ds.RasterCount >= 1
1446+
# Read a small window from an overview (if any) or from the full-res
1447+
# upper-left corner to exercise the fetch path without pulling
1448+
# too much data.
1449+
band = ds.GetRasterBand(1)
1450+
w = min(64, ds.RasterXSize)
1451+
h = min(64, ds.RasterYSize)
1452+
data = band.ReadRaster(0, 0, w, h, w, h)
1453+
assert data is not None and len(data) > 0
1454+
ds = None
1455+
finally:
1456+
gdal.VSICurlClearCache()
1457+
1458+
14191459
###############################################################################
14201460
# Test driver metadata
14211461

@@ -1429,3 +1469,169 @@ def test_jp2grok_driver_metadata():
14291469
assert drv.GetMetadataItem(gdal.DCAP_CREATECOPY) == "YES"
14301470
assert "jp2" in drv.GetMetadataItem(gdal.DMD_EXTENSIONS)
14311471
assert "j2k" in drv.GetMetadataItem(gdal.DMD_EXTENSIONS)
1472+
1473+
1474+
###############################################################################
1475+
# Webserver fixture for HTTP tests
1476+
1477+
1478+
@pytest.fixture(scope="module")
1479+
def server():
1480+
1481+
process, port = webserver.launch(handler=webserver.DispatcherHttpHandler)
1482+
if port == 0:
1483+
pytest.skip("cannot start HTTP server")
1484+
1485+
import collections
1486+
1487+
WebServer = collections.namedtuple("WebServer", "process port")
1488+
1489+
yield WebServer(process, port)
1490+
1491+
gdal.VSICurlClearCache()
1492+
webserver.server_stop(process, port)
1493+
1494+
1495+
###############################################################################
1496+
# Test: blocklisted HTTP settings force VSILFILE fallback.
1497+
#
1498+
# When an unsupported GDAL HTTP config option is set, GrokCanRead() should
1499+
# return false for /vsicurl/ paths, causing the driver to use GDAL's VSILFILE
1500+
# callbacks instead of Grok's native libcurl I/O. The dataset should still
1501+
# open successfully — just via the fallback path.
1502+
1503+
1504+
# Each entry is (config_option, value) that should trigger VSILFILE fallback.
1505+
_BLOCKLIST_CASES = [
1506+
("GDAL_HTTP_AUTH", "NTLM"),
1507+
("GDAL_HTTP_AUTH", "NEGOTIATE"),
1508+
("GDAL_HTTP_SSLCERT", "/path/to/cert.pem"),
1509+
("GDAL_HTTP_SSLKEY", "/path/to/key.pem"),
1510+
("GDAL_HTTP_SSLCERTTYPE", "PEM"),
1511+
("GDAL_HTTP_KEYPASSWD", "secret"),
1512+
("GDAL_HTTP_SSL_VERIFYSTATUS", "YES"),
1513+
("GDAL_CURL_CA_BUNDLE", "/path/to/ca-bundle.crt"),
1514+
("GDAL_HTTP_CAPATH", "/etc/ssl/certs"),
1515+
("GDAL_HTTP_HEADER_FILE", "/tmp/headers.txt"),
1516+
("GDAL_HTTPS_PROXY", "http://proxy:8443"),
1517+
("GDAL_PROXY_AUTH", "NTLM"),
1518+
("GDAL_HTTP_LOW_SPEED_TIME", "30"),
1519+
("GDAL_HTTP_LOW_SPEED_LIMIT", "1024"),
1520+
("GDAL_GSSAPI_DELEGATION", "POLICY"),
1521+
]
1522+
1523+
1524+
@pytest.mark.require_curl()
1525+
@pytest.mark.parametrize(
1526+
"option,value", _BLOCKLIST_CASES, ids=[c[0] for c in _BLOCKLIST_CASES]
1527+
)
1528+
def test_jp2grok_blocklist_fallback(server, option, value, tmp_path):
1529+
"""Blocklisted HTTP settings should trigger VSILFILE fallback while still
1530+
allowing the dataset to open successfully via GDAL's VSI layer."""
1531+
1532+
# GDAL_HTTP_HEADER_FILE requires the file to actually exist, otherwise
1533+
# GDAL logs an error when it tries to read headers from it.
1534+
if option == "GDAL_HTTP_HEADER_FILE":
1535+
header_file = tmp_path / "headers.txt"
1536+
header_file.write_text("X-Test: FallbackValue\n")
1537+
value = str(header_file)
1538+
1539+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1540+
gdal.VSICurlClearCache()
1541+
1542+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1543+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1544+
1545+
with gdal.config_option(option, value):
1546+
with webserver.install_http_handler(handler):
1547+
ds = gdal.Open(url)
1548+
# The dataset should open successfully via the VSILFILE callback
1549+
# path — GDAL's own curl handles the request.
1550+
assert ds is not None
1551+
assert ds.RasterXSize == 100
1552+
assert ds.RasterYSize == 100
1553+
ds = None
1554+
1555+
gdal.VSICurlClearCache()
1556+
1557+
1558+
###############################################################################
1559+
# Test: BASIC and BEARER auth should NOT trigger fallback (Grok handles these).
1560+
1561+
1562+
@pytest.mark.require_curl()
1563+
@pytest.mark.parametrize("auth_scheme", ["BASIC", "BEARER"])
1564+
def test_jp2grok_supported_auth_no_fallback(server, auth_scheme):
1565+
"""BASIC and BEARER auth are handled by Grok natively and should not
1566+
trigger the VSILFILE fallback path."""
1567+
1568+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1569+
gdal.VSICurlClearCache()
1570+
1571+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1572+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1573+
1574+
with gdal.config_option("GDAL_HTTP_AUTH", auth_scheme):
1575+
with webserver.install_http_handler(handler):
1576+
ds = gdal.Open(url)
1577+
assert ds is not None
1578+
assert ds.RasterXSize == 100
1579+
assert ds.RasterYSize == 100
1580+
ds = None
1581+
1582+
gdal.VSICurlClearCache()
1583+
1584+
1585+
###############################################################################
1586+
# Test: GDAL_HTTP_HEADERS with a single custom header should be forwarded
1587+
# to Grok's native I/O (no fallback).
1588+
1589+
1590+
@pytest.mark.require_curl()
1591+
def test_jp2grok_single_custom_header(server):
1592+
"""A single GDAL_HTTP_HEADERS entry should be forwarded to Grok's
1593+
custom_headers[] without triggering VSILFILE fallback."""
1594+
1595+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1596+
gdal.VSICurlClearCache()
1597+
1598+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1599+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1600+
1601+
with gdal.config_option("GDAL_HTTP_HEADERS", "X-Custom: TestValue"):
1602+
with webserver.install_http_handler(handler):
1603+
ds = gdal.Open(url)
1604+
assert ds is not None
1605+
assert ds.RasterXSize == 100
1606+
assert ds.RasterYSize == 100
1607+
ds = None
1608+
1609+
gdal.VSICurlClearCache()
1610+
1611+
1612+
###############################################################################
1613+
# Test: GDAL_HTTP_HEADERS with multiple headers should still work
1614+
# (forwarded to Grok's custom_headers[] array, up to GRK_MAX_CUSTOM_HEADERS).
1615+
1616+
1617+
@pytest.mark.require_curl()
1618+
def test_jp2grok_multiple_custom_headers(server):
1619+
"""Multiple GDAL_HTTP_HEADERS entries should be forwarded to Grok's
1620+
custom_headers[] array."""
1621+
1622+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1623+
gdal.VSICurlClearCache()
1624+
1625+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1626+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1627+
1628+
headers = "X-Custom1: Value1, X-Custom2: Value2, X-Custom3: Value3"
1629+
with gdal.config_option("GDAL_HTTP_HEADERS", headers):
1630+
with webserver.install_http_handler(handler):
1631+
ds = gdal.Open(url)
1632+
assert ds is not None
1633+
assert ds.RasterXSize == 100
1634+
assert ds.RasterYSize == 100
1635+
ds = None
1636+
1637+
gdal.VSICurlClearCache()

0 commit comments

Comments
 (0)