Skip to content

Commit d40ef10

Browse files
author
Grok Compression
committed
JP2Grok: support native Grok decompression of vsicurl files
1 parent 8dec0a3 commit d40ef10

4 files changed

Lines changed: 176 additions & 10 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.2.9
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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,45 @@ def test_jp2grok_transcode_ignored_options(tmp_path):
13501350
ds = None
13511351

13521352

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

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.2.9)

frmts/grok/grkdatasetbase.h

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,113 @@ template <size_t N> void safe_strcpy(char (&dest)[N], const std::string &src)
109109
dest[len] = '\0';
110110
}
111111

112+
#if defined(HAVE_CURL) && defined(GRK_HAS_LIBCURL)
113+
114+
/**
115+
* @brief True if @p filename is a network path handled natively by Grok.
116+
*
117+
* Both /vsis3/ and /vsicurl/ are fetched via libcurl inside Grok; /vsis3/
118+
* needs AWS credentials resolved by GDAL, /vsicurl/ only needs the shared
119+
* HTTP auth options (.netrc, cookies, allow-insecure).
120+
*/
121+
static bool isGrokNetworkPath(const char *filename)
122+
{
123+
return strncmp(filename, "/vsis3/", 7) == 0 ||
124+
strncmp(filename, "/vsicurl/", 9) == 0;
125+
}
126+
127+
/**
128+
* @brief Forward GDAL's shared HTTP auth config options to grk_stream_params.
129+
*
130+
* These options apply to any network transport (both /vsis3/ and /vsicurl/),
131+
* so they are set exactly once on the shared path
132+
* - GDAL_HTTP_UNSAFESSL -> s3_allow_insecure
133+
* - GDAL_HTTP_NETRC -> netrc (default YES, matching GDAL)
134+
* - GDAL_HTTP_NETRC_FILE -> netrc_file
135+
* - GDAL_HTTP_COOKIE -> cookie
136+
* - GDAL_HTTP_COOKIEFILE -> cookie_file
137+
* - GDAL_HTTP_COOKIEJAR -> cookie_jar
138+
* - GDAL_HTTP_USERPWD -> username / password
139+
* - GDAL_HTTP_BEARER -> bearer_token
140+
* - GDAL_HTTP_PROXY -> proxy
141+
* - GDAL_HTTP_PROXYUSERPWD -> proxy_userpwd
142+
* - GDAL_HTTP_USERAGENT -> user_agent
143+
* - GDAL_HTTP_TIMEOUT -> timeout
144+
* - GDAL_HTTP_CONNECTTIMEOUT -> connect_timeout
145+
* - GDAL_HTTP_MAX_RETRY -> max_retry
146+
* - GDAL_HTTP_RETRY_DELAY -> retry_delay
147+
*/
148+
static void forwardSharedHttpAuth(grk_stream_params &streamParams)
149+
{
150+
streamParams.s3_allow_insecure =
151+
CPLTestBool(CPLGetConfigOption("GDAL_HTTP_UNSAFESSL", "NO"));
152+
153+
streamParams.netrc =
154+
CPLTestBool(CPLGetConfigOption("GDAL_HTTP_NETRC", "YES"));
155+
if (const char *pszNetrcFile =
156+
CPLGetConfigOption("GDAL_HTTP_NETRC_FILE", nullptr))
157+
safe_strcpy(streamParams.netrc_file, pszNetrcFile);
158+
159+
if (const char *pszCookie = CPLGetConfigOption("GDAL_HTTP_COOKIE", nullptr))
160+
safe_strcpy(streamParams.cookie, pszCookie);
161+
if (const char *pszCookieFile =
162+
CPLGetConfigOption("GDAL_HTTP_COOKIEFILE", nullptr))
163+
safe_strcpy(streamParams.cookie_file, pszCookieFile);
164+
if (const char *pszCookieJar =
165+
CPLGetConfigOption("GDAL_HTTP_COOKIEJAR", nullptr))
166+
safe_strcpy(streamParams.cookie_jar, pszCookieJar);
167+
168+
// HTTP basic auth (user:password)
169+
if (const char *pszUserPwd =
170+
CPLGetConfigOption("GDAL_HTTP_USERPWD", nullptr))
171+
{
172+
const std::string osUserPwd(pszUserPwd);
173+
const size_t nColon = osUserPwd.find(':');
174+
if (nColon != std::string::npos)
175+
{
176+
safe_strcpy(streamParams.username, osUserPwd.substr(0, nColon));
177+
safe_strcpy(streamParams.password, osUserPwd.substr(nColon + 1));
178+
}
179+
}
180+
181+
// Bearer token for HTTP(S) endpoints
182+
if (const char *pszBearer = CPLGetConfigOption("GDAL_HTTP_BEARER", nullptr))
183+
safe_strcpy(streamParams.bearer_token, pszBearer);
184+
185+
// Proxy
186+
if (const char *pszProxy = CPLGetConfigOption("GDAL_HTTP_PROXY", nullptr))
187+
safe_strcpy(streamParams.proxy, pszProxy);
188+
if (const char *pszProxyUserPwd =
189+
CPLGetConfigOption("GDAL_HTTP_PROXYUSERPWD", nullptr))
190+
safe_strcpy(streamParams.proxy_userpwd, pszProxyUserPwd);
191+
192+
// User agent
193+
if (const char *pszUserAgent =
194+
CPLGetConfigOption("GDAL_HTTP_USERAGENT", nullptr))
195+
safe_strcpy(streamParams.user_agent, pszUserAgent);
196+
197+
// Timeouts
198+
const char *pszTimeout = CPLGetConfigOption("GDAL_HTTP_TIMEOUT", nullptr);
199+
if (pszTimeout)
200+
streamParams.timeout = atol(pszTimeout);
201+
const char *pszConnectTimeout =
202+
CPLGetConfigOption("GDAL_HTTP_CONNECTTIMEOUT", nullptr);
203+
if (pszConnectTimeout)
204+
streamParams.connect_timeout = atol(pszConnectTimeout);
205+
206+
// Retry configuration
207+
const char *pszMaxRetry =
208+
CPLGetConfigOption("GDAL_HTTP_MAX_RETRY", nullptr);
209+
if (pszMaxRetry)
210+
streamParams.max_retry = static_cast<uint32_t>(atol(pszMaxRetry));
211+
const char *pszRetryDelay =
212+
CPLGetConfigOption("GDAL_HTTP_RETRY_DELAY", nullptr);
213+
if (pszRetryDelay)
214+
streamParams.retry_delay = static_cast<uint32_t>(atol(pszRetryDelay));
215+
}
216+
217+
#endif // HAVE_CURL && GRK_HAS_LIBCURL
218+
112219
/************************************************************************/
113220
/* GrokCanRead() */
114221
/************************************************************************/
@@ -128,10 +235,12 @@ static bool GrokCanRead(const char *filename)
128235
return false;
129236

130237
#if defined(HAVE_CURL) && defined(GRK_HAS_LIBCURL)
131-
// Cloud storage: Grok handles S3 fetching natively via libcurl.
132-
// GDAL resolves AWS credentials and passes them to Grok via
133-
// grk_stream_params (username/password/bearer_token/region).
134-
if (strncmp(filename, "/vsis3/", 7) == 0)
238+
// Cloud storage: Grok handles fetching natively via libcurl.
239+
// For /vsis3/, GDAL resolves AWS credentials and passes them to Grok
240+
// via grk_stream_params. For /vsicurl/, Grok's HTTPFetcher strips the
241+
// prefix and issues HTTP(S) range requests directly, honoring the
242+
// shared .netrc/cookie options forwarded from GDAL.
243+
if (isGrokNetworkPath(filename))
135244
return true;
136245
#endif
137246

@@ -225,7 +334,7 @@ static void JP2_DebugCallback(const char *pszMsg, CPL_UNUSED void *unused)
225334
/**
226335
* @brief VSILFILE write callback for Grok stream I/O.
227336
*
228-
* Used when GrokCanRead() returns false (e.g. /vsimem/, /vsicurl/).
337+
* Used when GrokCanRead() returns false (e.g. /vsimem/, /vsitar/).
229338
* Also always used for compression output.
230339
*/
231340
static size_t JP2Dataset_Write(const uint8_t *pBuffer, size_t nBytes,
@@ -538,9 +647,21 @@ struct GRKCodecWrapper
538647
safe_strcpy(streamParams.file, pszFilename);
539648
streamParams.initial_offset = psJP2File->nBaseOffset;
540649
#if defined(HAVE_CURL) && defined(GRK_HAS_LIBCURL)
650+
// Shared HTTP auth options (.netrc, cookies, allow-insecure)
651+
// apply to both /vsis3/ and /vsicurl/ and are set exactly once
652+
// here so the S3-specific branch below does not duplicate them.
653+
if (isGrokNetworkPath(pszFilename))
654+
forwardSharedHttpAuth(streamParams);
655+
541656
// For /vsis3/ paths, resolve AWS credentials through GDAL's
542657
// full authentication chain and pass them to Grok so it can
543-
// handle S3 fetching natively via libcurl.
658+
// handle S3 fetching natively via libcurl. For /vsicurl/,
659+
// Grok's HTTPFetcher strips the prefix and honors the shared
660+
// HTTP auth options forwarded above.
661+
//
662+
// S3 credentials override the generic USERPWD / BEARER values
663+
// set by forwardSharedHttpAuth, since the S3 signer requires
664+
// the access-key / secret / session-token triple.
544665
if (strncmp(pszFilename, "/vsis3/", 7) == 0)
545666
{
546667
auto poHelper = std::unique_ptr<VSIS3HandleHelper>(
@@ -572,8 +693,13 @@ struct GRKCodecWrapper
572693
streamParams.s3_no_sign_request =
573694
poHelper->GetCredentialsSource() ==
574695
AWSCredentialsSource::NO_SIGN_REQUEST;
575-
streamParams.s3_allow_insecure = CPLTestBool(
576-
CPLGetConfigOption("GDAL_HTTP_UNSAFESSL", "NO"));
696+
697+
// Requester pays
698+
const char *pszRequestPayer = VSIGetPathSpecificOption(
699+
pszFilename, "AWS_REQUEST_PAYER", "");
700+
if (pszRequestPayer[0])
701+
safe_strcpy(streamParams.request_payer,
702+
pszRequestPayer);
577703
}
578704
else
579705
{
@@ -1330,6 +1456,7 @@ struct GRKCodecWrapper
13301456
*
13311457
* For local files and /vsis3/, uses native file I/O (closing the
13321458
* VSILFILE). For other VSI paths, uses VSILFILE callbacks.
1459+
* (/vsicurl/ is read-only so it never reaches the compression path.)
13331460
*/
13341461
bool initCodec(const char *pszFilename, VSIVirtualHandleUniquePtr &fpOwner)
13351462
{

0 commit comments

Comments
 (0)