Skip to content

Commit d0b4dac

Browse files
author
Grok Compression
committed
JP2Grok: support native Grok decompression of vsicurl files
1 parent fd3f16d commit d0b4dac

4 files changed

Lines changed: 468 additions & 11 deletions

File tree

.github/workflows/ubuntu_26.04/Dockerfile.ci

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ RUN curl -LO -fsS https://github.com/apache/arrow/archive/refs/heads/main.zip \
230230
&& rm -rf arrow-main
231231

232232
# Build Grok JPEG 2000 library
233-
ARG GROK_VERSION=v20.2.8
233+
ARG GROK_VERSION=v20.3.0
234234

235235
RUN git clone --recursive --depth 1 --branch ${GROK_VERSION} \
236236
https://github.com/GrokImageCompression/grok.git grok-git && \

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
@@ -1350,6 +1351,45 @@ def test_jp2grok_transcode_ignored_options(tmp_path):
13501351
ds = None
13511352

13521353

1354+
###############################################################################
1355+
# Test reading a remote JP2 via /vsicurl/ (handled natively by Grok's libcurl
1356+
# backend when available). Uses a real URL, so it is only run when slow
1357+
# tests are enabled.
1358+
1359+
1360+
def test_jp2grok_vsicurl_remote():
1361+
1362+
if not gdaltest.run_slow_tests():
1363+
pytest.skip("GDAL_RUN_SLOW_TESTS not set")
1364+
if "CURL_ENABLED=YES" not in gdal.VersionInfo("BUILD_INFO"):
1365+
pytest.skip("curl not enabled in this GDAL build")
1366+
1367+
url = (
1368+
"/vsicurl/https://www.opengeodata.nrw.de/produkte/geobasis/lusat/"
1369+
"akt/dop/dop_jp2_f10/dop10rgbi_32_280_5653_1_nw_2025.jp2"
1370+
)
1371+
1372+
gdal.VSICurlClearCache()
1373+
try:
1374+
ds = gdal.Open(url)
1375+
if ds is None:
1376+
pytest.skip("remote host unreachable: " + gdal.GetLastErrorMsg())
1377+
assert ds.RasterXSize > 0
1378+
assert ds.RasterYSize > 0
1379+
assert ds.RasterCount >= 1
1380+
# Read a small window from an overview (if any) or from the full-res
1381+
# upper-left corner to exercise the fetch path without pulling
1382+
# too much data.
1383+
band = ds.GetRasterBand(1)
1384+
w = min(64, ds.RasterXSize)
1385+
h = min(64, ds.RasterYSize)
1386+
data = band.ReadRaster(0, 0, w, h, w, h)
1387+
assert data is not None and len(data) > 0
1388+
ds = None
1389+
finally:
1390+
gdal.VSICurlClearCache()
1391+
1392+
13531393
###############################################################################
13541394
# Test driver metadata
13551395

@@ -1363,3 +1403,169 @@ def test_jp2grok_driver_metadata():
13631403
assert drv.GetMetadataItem(gdal.DCAP_CREATECOPY) == "YES"
13641404
assert "jp2" in drv.GetMetadataItem(gdal.DMD_EXTENSIONS)
13651405
assert "j2k" in drv.GetMetadataItem(gdal.DMD_EXTENSIONS)
1406+
1407+
1408+
###############################################################################
1409+
# Webserver fixture for HTTP tests
1410+
1411+
1412+
@pytest.fixture(scope="module")
1413+
def server():
1414+
1415+
process, port = webserver.launch(handler=webserver.DispatcherHttpHandler)
1416+
if port == 0:
1417+
pytest.skip("cannot start HTTP server")
1418+
1419+
import collections
1420+
1421+
WebServer = collections.namedtuple("WebServer", "process port")
1422+
1423+
yield WebServer(process, port)
1424+
1425+
gdal.VSICurlClearCache()
1426+
webserver.server_stop(process, port)
1427+
1428+
1429+
###############################################################################
1430+
# Test: blocklisted HTTP settings force VSILFILE fallback.
1431+
#
1432+
# When an unsupported GDAL HTTP config option is set, GrokCanRead() should
1433+
# return false for /vsicurl/ paths, causing the driver to use GDAL's VSILFILE
1434+
# callbacks instead of Grok's native libcurl I/O. The dataset should still
1435+
# open successfully — just via the fallback path.
1436+
1437+
1438+
# Each entry is (config_option, value) that should trigger VSILFILE fallback.
1439+
_BLOCKLIST_CASES = [
1440+
("GDAL_HTTP_AUTH", "NTLM"),
1441+
("GDAL_HTTP_AUTH", "NEGOTIATE"),
1442+
("GDAL_HTTP_SSLCERT", "/path/to/cert.pem"),
1443+
("GDAL_HTTP_SSLKEY", "/path/to/key.pem"),
1444+
("GDAL_HTTP_SSLCERTTYPE", "PEM"),
1445+
("GDAL_HTTP_KEYPASSWD", "secret"),
1446+
("GDAL_HTTP_SSL_VERIFYSTATUS", "YES"),
1447+
("GDAL_CURL_CA_BUNDLE", "/path/to/ca-bundle.crt"),
1448+
("GDAL_HTTP_CAPATH", "/etc/ssl/certs"),
1449+
("GDAL_HTTP_HEADER_FILE", "/tmp/headers.txt"),
1450+
("GDAL_HTTPS_PROXY", "http://proxy:8443"),
1451+
("GDAL_PROXY_AUTH", "NTLM"),
1452+
("GDAL_HTTP_LOW_SPEED_TIME", "30"),
1453+
("GDAL_HTTP_LOW_SPEED_LIMIT", "1024"),
1454+
("GDAL_GSSAPI_DELEGATION", "POLICY"),
1455+
]
1456+
1457+
1458+
@pytest.mark.require_curl()
1459+
@pytest.mark.parametrize(
1460+
"option,value", _BLOCKLIST_CASES, ids=[c[0] for c in _BLOCKLIST_CASES]
1461+
)
1462+
def test_jp2grok_blocklist_fallback(server, option, value, tmp_path):
1463+
"""Blocklisted HTTP settings should trigger VSILFILE fallback while still
1464+
allowing the dataset to open successfully via GDAL's VSI layer."""
1465+
1466+
# GDAL_HTTP_HEADER_FILE requires the file to actually exist, otherwise
1467+
# GDAL logs an error when it tries to read headers from it.
1468+
if option == "GDAL_HTTP_HEADER_FILE":
1469+
header_file = tmp_path / "headers.txt"
1470+
header_file.write_text("X-Test: FallbackValue\n")
1471+
value = str(header_file)
1472+
1473+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1474+
gdal.VSICurlClearCache()
1475+
1476+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1477+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1478+
1479+
with gdal.config_option(option, value):
1480+
with webserver.install_http_handler(handler):
1481+
ds = gdal.Open(url)
1482+
# The dataset should open successfully via the VSILFILE callback
1483+
# path — GDAL's own curl handles the request.
1484+
assert ds is not None
1485+
assert ds.RasterXSize == 100
1486+
assert ds.RasterYSize == 100
1487+
ds = None
1488+
1489+
gdal.VSICurlClearCache()
1490+
1491+
1492+
###############################################################################
1493+
# Test: BASIC and BEARER auth should NOT trigger fallback (Grok handles these).
1494+
1495+
1496+
@pytest.mark.require_curl()
1497+
@pytest.mark.parametrize("auth_scheme", ["BASIC", "BEARER"])
1498+
def test_jp2grok_supported_auth_no_fallback(server, auth_scheme):
1499+
"""BASIC and BEARER auth are handled by Grok natively and should not
1500+
trigger the VSILFILE fallback path."""
1501+
1502+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1503+
gdal.VSICurlClearCache()
1504+
1505+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1506+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1507+
1508+
with gdal.config_option("GDAL_HTTP_AUTH", auth_scheme):
1509+
with webserver.install_http_handler(handler):
1510+
ds = gdal.Open(url)
1511+
assert ds is not None
1512+
assert ds.RasterXSize == 100
1513+
assert ds.RasterYSize == 100
1514+
ds = None
1515+
1516+
gdal.VSICurlClearCache()
1517+
1518+
1519+
###############################################################################
1520+
# Test: GDAL_HTTP_HEADERS with a single custom header should be forwarded
1521+
# to Grok's native I/O (no fallback).
1522+
1523+
1524+
@pytest.mark.require_curl()
1525+
def test_jp2grok_single_custom_header(server):
1526+
"""A single GDAL_HTTP_HEADERS entry should be forwarded to Grok's
1527+
custom_headers[] without triggering VSILFILE fallback."""
1528+
1529+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1530+
gdal.VSICurlClearCache()
1531+
1532+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1533+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1534+
1535+
with gdal.config_option("GDAL_HTTP_HEADERS", "X-Custom: TestValue"):
1536+
with webserver.install_http_handler(handler):
1537+
ds = gdal.Open(url)
1538+
assert ds is not None
1539+
assert ds.RasterXSize == 100
1540+
assert ds.RasterYSize == 100
1541+
ds = None
1542+
1543+
gdal.VSICurlClearCache()
1544+
1545+
1546+
###############################################################################
1547+
# Test: GDAL_HTTP_HEADERS with multiple headers should still work
1548+
# (forwarded to Grok's custom_headers[] array, up to GRK_MAX_CUSTOM_HEADERS).
1549+
1550+
1551+
@pytest.mark.require_curl()
1552+
def test_jp2grok_multiple_custom_headers(server):
1553+
"""Multiple GDAL_HTTP_HEADERS entries should be forwarded to Grok's
1554+
custom_headers[] array."""
1555+
1556+
jp2_data = open("data/jpeg2000/byte.jp2", "rb").read()
1557+
gdal.VSICurlClearCache()
1558+
1559+
handler = webserver.FileHandler({"/byte.jp2": jp2_data})
1560+
url = "/vsicurl/http://localhost:%d/byte.jp2" % server.port
1561+
1562+
headers = "X-Custom1: Value1, X-Custom2: Value2, X-Custom3: Value3"
1563+
with gdal.config_option("GDAL_HTTP_HEADERS", headers):
1564+
with webserver.install_http_handler(handler):
1565+
ds = gdal.Open(url)
1566+
assert ds is not None
1567+
assert ds.RasterXSize == 100
1568+
assert ds.RasterYSize == 100
1569+
ds = None
1570+
1571+
gdal.VSICurlClearCache()

cmake/helpers/CheckDependentLibrariesGrok.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ gdal_check_package(Grok "Enable JPEG2000 support with Grok library"
22
CONFIG
33
CAN_DISABLE
44
TARGETS GROK::grokj2k
5-
VERSION 20.2.8)
5+
VERSION 20.3.0)

0 commit comments

Comments
 (0)