Line data Source code
1 : /******************************************************************************
2 : *
3 : * Purpose : gdal driver for reading Esri compact cache as raster
4 : * based on public documentation available at
5 : * https://github.com/Esri/raster-tiles-compactcache
6 : *
7 : * Author : Lucian Plesea
8 : *
9 : * Udate : 06 / 10 / 2020
10 : *
11 : * Copyright 2020 Esri
12 : *
13 : * SPDX-License-Identifier: MIT
14 : *****************************************************************************/
15 :
16 : #include "gdal_priv.h"
17 : #include <cassert>
18 : #include <vector>
19 : #include <algorithm>
20 : #include "cpl_json.h"
21 : #include "gdal_proxy.h"
22 : #include "gdal_utils.h"
23 : #include "cpl_vsi_virtual.h"
24 :
25 : using namespace std;
26 :
27 : CPL_C_START
28 : void CPL_DLL GDALRegister_ESRIC();
29 : CPL_C_END
30 :
31 : namespace ESRIC
32 : {
33 :
34 : #define ENDS_WITH_CI(a, b) \
35 : (strlen(a) >= strlen(b) && EQUAL(a + strlen(a) - strlen(b), b))
36 :
37 : // ESRI tpkx files use root.json
38 66777 : static int IdentifyJSON(GDALOpenInfo *poOpenInfo)
39 : {
40 66777 : if (poOpenInfo->eAccess != GA_ReadOnly || poOpenInfo->nHeaderBytes < 512)
41 59946 : return false;
42 :
43 : // Recognize .tpkx file directly passed
44 6831 : if (!STARTS_WITH(poOpenInfo->pszFilename, "/vsizip/") &&
45 : #if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
46 6813 : ENDS_WITH_CI(poOpenInfo->pszFilename, ".tpkx") &&
47 : #endif
48 14 : memcmp(poOpenInfo->pabyHeader, "PK\x03\x04", 4) == 0)
49 : {
50 14 : return true;
51 : }
52 :
53 : #if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
54 6817 : if (!ENDS_WITH_CI(poOpenInfo->pszFilename, "root.json"))
55 6802 : return false;
56 : #endif
57 16 : for (int i = 0; i < 2; ++i)
58 : {
59 : const std::string osHeader(
60 16 : reinterpret_cast<char *>(poOpenInfo->pabyHeader),
61 16 : poOpenInfo->nHeaderBytes);
62 16 : if (std::string::npos != osHeader.find("tileBundlesPath"))
63 : {
64 15 : return true;
65 : }
66 : // If we didn't find tileBundlesPath i, the first bytes, but find
67 : // other elements typically of .tpkx, then ingest more bytes and
68 : // retry
69 1 : constexpr int MORE_BYTES = 8192;
70 3 : if (poOpenInfo->nHeaderBytes < MORE_BYTES &&
71 2 : (std::string::npos != osHeader.find("tileInfo") ||
72 1 : std::string::npos != osHeader.find("tileImageInfo")))
73 : {
74 1 : poOpenInfo->TryToIngest(MORE_BYTES);
75 : }
76 : else
77 0 : break;
78 : }
79 0 : return false;
80 : }
81 :
82 : // Without full XML parsing, weak, might still fail
83 66789 : static int IdentifyXML(GDALOpenInfo *poOpenInfo)
84 : {
85 66789 : if (poOpenInfo->eAccess != GA_ReadOnly
86 : #if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
87 66283 : || !ENDS_WITH_CI(poOpenInfo->pszFilename, "conf.xml")
88 : #endif
89 12 : || poOpenInfo->nHeaderBytes < 512)
90 66777 : return false;
91 12 : CPLString header(reinterpret_cast<char *>(poOpenInfo->pabyHeader),
92 12 : poOpenInfo->nHeaderBytes);
93 12 : return (CPLString::npos != header.find("<CacheInfo"));
94 : }
95 :
96 66765 : static int Identify(GDALOpenInfo *poOpenInfo)
97 : {
98 66765 : return (IdentifyXML(poOpenInfo) || IdentifyJSON(poOpenInfo));
99 : }
100 :
101 : // Stub default delete, don't delete a tile cache from GDAL
102 0 : static CPLErr Delete(const char *)
103 : {
104 0 : return CE_None;
105 : }
106 :
107 : // Read a 32bit unsigned integer stored in little endian
108 : // Same as CPL_LSBUINT32PTR
109 35 : static inline GUInt32 u32lat(void *data)
110 : {
111 : GUInt32 val;
112 35 : memcpy(&val, data, 4);
113 35 : return CPL_LSBWORD32(val);
114 : }
115 :
116 : struct Bundle
117 : {
118 8 : void Init(const char *filename)
119 : {
120 8 : name = filename;
121 8 : fh.reset(VSIFOpenL(name.c_str(), "rb"));
122 8 : if (nullptr == fh)
123 1 : return;
124 7 : GByte header[64] = {0};
125 : // Check a few header locations, then read the index
126 7 : fh->Read(header, 1, 64);
127 7 : index.resize(BSZ * BSZ);
128 21 : if (3 != u32lat(header) || 5 != u32lat(header + 12) ||
129 14 : 40 != u32lat(header + 32) || 0 != u32lat(header + 36) ||
130 21 : BSZ * BSZ * 8 != u32lat(header + 60) ||
131 7 : index.size() != fh->Read(index.data(), 8, index.size()))
132 : {
133 0 : fh.reset();
134 : }
135 :
136 : if constexpr (!CPL_IS_LSB)
137 : {
138 : for (auto &v : index)
139 : CPL_LSBPTR64(&v);
140 : }
141 : }
142 :
143 : std::vector<GUInt64> index{};
144 : VSIVirtualHandleUniquePtr fh{};
145 : bool isV2 = false;
146 : CPLString name{};
147 : const size_t BSZ = 128;
148 : };
149 :
150 : class ECDataset final : public GDALDataset
151 : {
152 : friend class ECBand;
153 :
154 : public:
155 : ECDataset();
156 :
157 12 : CPLErr GetGeoTransform(GDALGeoTransform >) const override
158 : {
159 12 : gt = m_gt;
160 12 : return CE_None;
161 : }
162 :
163 : const OGRSpatialReference *GetSpatialRef() const override;
164 :
165 : static GDALDataset *Open(GDALOpenInfo *poOpenInfo);
166 : static GDALDataset *Open(GDALOpenInfo *poOpenInfo,
167 : const char *pszDescription);
168 :
169 : protected:
170 : GDALGeoTransform m_gt{};
171 : CPLString dname{};
172 : int isV2{}; // V2 bundle format
173 : int BSZ{}; // Bundle size in tiles
174 : int TSZ{}; // Tile size in pixels
175 : std::vector<Bundle> bundles{};
176 :
177 : Bundle &GetBundle(const char *fname);
178 :
179 : private:
180 : CPLErr Initialize(CPLXMLNode *CacheInfo, bool ignoreOversizedLods);
181 : CPLErr InitializeFromJSON(const CPLJSONObject &oRoot,
182 : bool ignoreOversizedLods);
183 : CPLString compression{};
184 : std::vector<double> resolutions{};
185 : int m_nMinLOD = 0;
186 : OGRSpatialReference oSRS{};
187 : std::vector<GByte> tilebuffer{}; // Last read tile, decompressed
188 : std::vector<GByte> filebuffer{}; // raw tile buffer
189 :
190 : OGREnvelope m_sInitialExtent{};
191 : OGREnvelope m_sFullExtent{};
192 : };
193 :
194 7 : const OGRSpatialReference *ECDataset::GetSpatialRef() const
195 : {
196 7 : return &oSRS;
197 : }
198 :
199 : class ECBand final : public GDALRasterBand
200 : {
201 : friend class ECDataset;
202 :
203 : public:
204 : ECBand(ECDataset *parent, int b, int level = 0);
205 : ~ECBand() override;
206 :
207 : CPLErr IReadBlock(int xblk, int yblk, void *buffer) override;
208 :
209 25 : GDALColorInterp GetColorInterpretation() override
210 : {
211 25 : return ci;
212 : }
213 :
214 150 : int GetOverviewCount() override
215 : {
216 150 : return static_cast<int>(overviews.size());
217 : }
218 :
219 126 : GDALRasterBand *GetOverview(int n) override
220 : {
221 126 : return (n >= 0 && n < GetOverviewCount()) ? overviews[n] : nullptr;
222 : }
223 :
224 : protected:
225 : private:
226 : int lvl{};
227 : GDALColorInterp ci{};
228 :
229 : // Image image;
230 : void AddOverviews();
231 : std::vector<ECBand *> overviews{};
232 : };
233 :
234 17 : ECDataset::ECDataset() : isV2(true), BSZ(128), TSZ(256)
235 : {
236 17 : }
237 :
238 6 : CPLErr ECDataset::Initialize(CPLXMLNode *CacheInfo, bool ignoreOversizedLods)
239 : {
240 6 : CPLErr error = CE_None;
241 : try
242 : {
243 6 : CPLXMLNode *CSI = CPLGetXMLNode(CacheInfo, "CacheStorageInfo");
244 6 : CPLXMLNode *TCI = CPLGetXMLNode(CacheInfo, "TileCacheInfo");
245 6 : if (!CSI || !TCI)
246 0 : throw CPLString("Error parsing cache configuration");
247 6 : auto format = CPLGetXMLValue(CSI, "StorageFormat", "");
248 6 : isV2 = EQUAL(format, "esriMapCacheStorageModeCompactV2");
249 6 : if (!isV2)
250 0 : throw CPLString("Not recognized as esri V2 bundled cache");
251 6 : if (BSZ != CPLAtof(CPLGetXMLValue(CSI, "PacketSize", "128")))
252 0 : throw CPLString("Only PacketSize of 128 is supported");
253 6 : TSZ = static_cast<int>(CPLAtof(CPLGetXMLValue(TCI, "TileCols", "256")));
254 6 : if (TSZ != CPLAtof(CPLGetXMLValue(TCI, "TileRows", "256")))
255 0 : throw CPLString("Non-square tiles are not supported");
256 6 : if (TSZ < 0 || TSZ > 8192)
257 0 : throw CPLString("Unsupported TileCols value");
258 :
259 6 : double minx = CPLAtof(CPLGetXMLValue(TCI, "TileOrigin.X", "-180"));
260 6 : double maxy = CPLAtof(CPLGetXMLValue(TCI, "TileOrigin.Y", "90"));
261 : // Assume symmetric coverage, check custom end
262 6 : double maxx = -minx;
263 6 : double miny = -maxy;
264 6 : const char *pszmaxx = CPLGetXMLValue(TCI, "TileEnd.X", nullptr);
265 6 : const char *pszminy = CPLGetXMLValue(TCI, "TileEnd.Y", nullptr);
266 6 : if (pszmaxx && pszminy)
267 : {
268 6 : maxx = CPLAtof(pszmaxx);
269 6 : miny = CPLAtof(pszminy);
270 : }
271 :
272 6 : CPLXMLNode *LODInfo = CPLGetXMLNode(TCI, "LODInfos.LODInfo");
273 6 : double res = 0;
274 91 : while (LODInfo)
275 : {
276 87 : res = CPLAtof(CPLGetXMLValue(LODInfo, "Resolution", "0"));
277 87 : if (!(res > 0))
278 0 : throw CPLString("Can't parse resolution for LOD");
279 :
280 87 : double dxsz = (maxx - minx) / res;
281 87 : double dysz = (maxy - miny) / res;
282 : // Allow size just above INT32_MAX to handle FP rounding. Actual size is later clamped to INT32_MAX
283 87 : double maxRasterSize = static_cast<double>(INT32_MAX) + 2;
284 87 : if (dxsz < 1 || dxsz > maxRasterSize || dysz < 1 ||
285 : dysz > maxRasterSize)
286 : {
287 3 : if (ignoreOversizedLods)
288 : {
289 1 : CPLDebug(
290 : "ESRIC",
291 : "Skipping resolution %.10f: raster size exceeds the "
292 : "GDAL limit",
293 : res);
294 : }
295 : else
296 : {
297 : throw CPLString(
298 : "Too many levels, resulting raster size exceeds "
299 : "the GDAL limit. Open with IGNORE_OVERSIZED_LODS=YES "
300 2 : "to ignore this");
301 : }
302 : }
303 : else
304 : {
305 84 : resolutions.push_back(res);
306 : }
307 :
308 85 : LODInfo = LODInfo->psNext;
309 : }
310 :
311 4 : sort(resolutions.begin(), resolutions.end());
312 4 : if (resolutions.empty())
313 0 : throw CPLString("Can't parse LODInfos");
314 :
315 : CPLString RawProj(
316 8 : CPLGetXMLValue(TCI, "SpatialReference.WKT", "EPSG:4326"));
317 4 : if (OGRERR_NONE != oSRS.SetFromUserInput(RawProj.c_str()))
318 0 : throw CPLString("Invalid Spatial Reference");
319 4 : oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
320 :
321 : // resolution is the smallest figure
322 4 : res = resolutions[0];
323 4 : m_gt = GDALGeoTransform();
324 4 : m_gt[0] = minx;
325 4 : m_gt[3] = maxy;
326 4 : m_gt[1] = res;
327 4 : m_gt[5] = -res;
328 :
329 4 : double dxsz = (maxx - minx) / res;
330 4 : double dysz = (maxy - miny) / res;
331 :
332 4 : nRasterXSize = int(std::min(dxsz, double(INT32_MAX)));
333 4 : nRasterYSize = int(std::min(dysz, double(INT32_MAX)));
334 :
335 4 : SetMetadataItem("INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE");
336 : compression =
337 4 : CPLGetXMLValue(CacheInfo, "TileImageInfo.CacheTileFormat", "JPEG");
338 4 : SetMetadataItem("COMPRESS", compression.c_str(), "IMAGE_STRUCTURE");
339 :
340 4 : nBands = EQUAL(compression, "JPEG") ? 3 : 4;
341 20 : for (int i = 1; i <= nBands; i++)
342 : {
343 16 : ECBand *band = new ECBand(this, i);
344 16 : SetBand(i, band);
345 : }
346 : // Keep 4 bundle files open
347 4 : bundles.resize(4);
348 : }
349 4 : catch (CPLString &err)
350 : {
351 2 : error = CE_Failure;
352 2 : CPLError(error, CPLE_OpenFailed, "%s", err.c_str());
353 : }
354 6 : return error;
355 : }
356 :
357 : static std::unique_ptr<OGRSpatialReference>
358 27 : CreateSRS(const CPLJSONObject &oSRSRoot)
359 : {
360 54 : auto poSRS = std::make_unique<OGRSpatialReference>();
361 :
362 27 : bool bSuccess = false;
363 27 : const int nCode = oSRSRoot.GetInteger("wkid");
364 : // The concept of LatestWKID is explained in
365 : // https://support.esri.com/en/technical-article/000013950
366 27 : const int nLatestCode = oSRSRoot.GetInteger("latestWkid");
367 :
368 : // Try first with nLatestWKID as there is a higher chance it is a
369 : // EPSG code and not an ESRI one.
370 27 : if (nLatestCode > 0)
371 : {
372 27 : if (nLatestCode > 32767)
373 : {
374 0 : if (poSRS->SetFromUserInput(CPLSPrintf("ESRI:%d", nLatestCode)) ==
375 : OGRERR_NONE)
376 : {
377 0 : bSuccess = true;
378 : }
379 : }
380 27 : else if (poSRS->importFromEPSG(nLatestCode) == OGRERR_NONE)
381 : {
382 27 : bSuccess = true;
383 : }
384 : }
385 27 : if (!bSuccess && nCode > 0)
386 : {
387 0 : if (nCode > 32767)
388 : {
389 0 : if (poSRS->SetFromUserInput(CPLSPrintf("ESRI:%d", nCode)) ==
390 : OGRERR_NONE)
391 : {
392 0 : bSuccess = true;
393 : }
394 : }
395 0 : else if (poSRS->importFromEPSG(nCode) == OGRERR_NONE)
396 : {
397 0 : bSuccess = true;
398 : }
399 : }
400 27 : if (!bSuccess)
401 : {
402 0 : return nullptr;
403 : }
404 :
405 27 : poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
406 27 : return poSRS;
407 : }
408 :
409 11 : CPLErr ECDataset::InitializeFromJSON(const CPLJSONObject &oRoot,
410 : bool ignoreOversizedLods)
411 : {
412 11 : CPLErr error = CE_None;
413 : try
414 : {
415 33 : auto format = oRoot.GetString("storageInfo/storageFormat");
416 11 : isV2 = EQUAL(format.c_str(), "esriMapCacheStorageModeCompactV2");
417 11 : if (!isV2)
418 0 : throw CPLString("Not recognized as esri V2 bundled cache");
419 11 : if (BSZ != oRoot.GetInteger("storageInfo/packetSize"))
420 0 : throw CPLString("Only PacketSize of 128 is supported");
421 :
422 11 : TSZ = oRoot.GetInteger("tileInfo/rows");
423 11 : if (TSZ != oRoot.GetInteger("tileInfo/cols"))
424 0 : throw CPLString("Non-square tiles are not supported");
425 11 : if (TSZ < 0 || TSZ > 8192)
426 0 : throw CPLString("Unsupported tileInfo/rows value");
427 :
428 11 : double minx = oRoot.GetDouble("tileInfo/origin/x");
429 11 : double maxy = oRoot.GetDouble("tileInfo/origin/y");
430 : // Assume symmetric coverage
431 11 : double maxx = -minx;
432 11 : double miny = -maxy;
433 :
434 33 : const auto oLODs = oRoot.GetArray("tileInfo/lods");
435 11 : double res = 0;
436 : // we need to skip levels that don't have bundle files
437 11 : m_nMinLOD = oRoot.GetInteger("minLOD");
438 11 : if (m_nMinLOD < 0 || m_nMinLOD >= 31)
439 0 : throw CPLString("Invalid minLOD");
440 11 : const int maxLOD = std::min(oRoot.GetInteger("maxLOD"), 31);
441 280 : for (const auto &oLOD : oLODs)
442 : {
443 267 : const int level = oLOD.GetInteger("level");
444 267 : if (level < m_nMinLOD || level > maxLOD)
445 149 : continue;
446 :
447 118 : res = oLOD.GetDouble("resolution");
448 118 : if (!(res > 0))
449 0 : throw CPLString("Can't parse resolution for LOD");
450 :
451 118 : double dxsz = (maxx - minx) / res;
452 118 : double dysz = (maxy - miny) / res;
453 : // Allow size just above INT32_MAX to handle FP rounding. Actual size is later clamped to INT32_MAX
454 118 : double maxRasterSize = static_cast<double>(INT32_MAX) + 2;
455 118 : if (dxsz < 1 || dxsz > maxRasterSize || dysz < 1 ||
456 : dysz > maxRasterSize)
457 : {
458 3 : if (ignoreOversizedLods)
459 : {
460 1 : CPLDebug("ESRIC",
461 : "Skipping LOD with resolution %.10f: raster size "
462 : "exceeds the GDAL limit",
463 : res);
464 1 : continue;
465 : }
466 : else
467 : {
468 : throw CPLString(
469 : "Too many levels, resulting raster size exceeds "
470 : "the GDAL limit. Open with IGNORE_OVERSIZED_LODS=YES "
471 2 : "to ignore this");
472 : }
473 : }
474 :
475 115 : resolutions.push_back(res);
476 : }
477 9 : sort(resolutions.begin(), resolutions.end());
478 9 : if (resolutions.empty())
479 0 : throw CPLString("Can't parse lods");
480 :
481 : {
482 27 : auto poSRS = CreateSRS(oRoot.GetObj("spatialReference"));
483 9 : if (!poSRS)
484 : {
485 0 : throw CPLString("Invalid Spatial Reference");
486 : }
487 9 : oSRS = std::move(*poSRS);
488 : }
489 :
490 : // resolution is the smallest figure
491 9 : res = resolutions[0];
492 9 : m_gt = GDALGeoTransform();
493 9 : m_gt[0] = minx;
494 9 : m_gt[3] = maxy;
495 9 : m_gt[1] = res;
496 9 : m_gt[5] = -res;
497 :
498 9 : double dxsz = (maxx - minx) / res;
499 9 : double dysz = (maxy - miny) / res;
500 :
501 9 : nRasterXSize = int(std::min(dxsz, double(INT32_MAX)));
502 9 : nRasterYSize = int(std::min(dysz, double(INT32_MAX)));
503 :
504 9 : SetMetadataItem("INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE");
505 9 : compression = oRoot.GetString("tileImageInfo/format");
506 9 : SetMetadataItem("COMPRESS", compression.c_str(), "IMAGE_STRUCTURE");
507 :
508 27 : auto oInitialExtent = oRoot.GetObj("initialExtent");
509 18 : if (oInitialExtent.IsValid() &&
510 9 : oInitialExtent.GetType() == CPLJSONObject::Type::Object)
511 : {
512 9 : m_sInitialExtent.MinX = oInitialExtent.GetDouble("xmin");
513 9 : m_sInitialExtent.MinY = oInitialExtent.GetDouble("ymin");
514 9 : m_sInitialExtent.MaxX = oInitialExtent.GetDouble("xmax");
515 9 : m_sInitialExtent.MaxY = oInitialExtent.GetDouble("ymax");
516 27 : auto oSRSRoot = oInitialExtent.GetObj("spatialReference");
517 9 : if (oSRSRoot.IsValid())
518 : {
519 18 : auto poSRS = CreateSRS(oSRSRoot);
520 9 : if (!poSRS)
521 : {
522 : throw CPLString(
523 0 : "Invalid Spatial Reference in initialExtent");
524 : }
525 9 : if (!poSRS->IsSame(&oSRS))
526 : {
527 0 : CPLError(CE_Warning, CPLE_AppDefined,
528 : "Ignoring initialExtent, because its SRS is "
529 : "different from the main one");
530 0 : m_sInitialExtent = OGREnvelope();
531 : }
532 : }
533 : }
534 :
535 27 : auto oFullExtent = oRoot.GetObj("fullExtent");
536 18 : if (oFullExtent.IsValid() &&
537 9 : oFullExtent.GetType() == CPLJSONObject::Type::Object)
538 : {
539 9 : m_sFullExtent.MinX = oFullExtent.GetDouble("xmin");
540 9 : m_sFullExtent.MinY = oFullExtent.GetDouble("ymin");
541 9 : m_sFullExtent.MaxX = oFullExtent.GetDouble("xmax");
542 9 : m_sFullExtent.MaxY = oFullExtent.GetDouble("ymax");
543 27 : auto oSRSRoot = oFullExtent.GetObj("spatialReference");
544 9 : if (oSRSRoot.IsValid())
545 : {
546 18 : auto poSRS = CreateSRS(oSRSRoot);
547 9 : if (!poSRS)
548 : {
549 0 : throw CPLString("Invalid Spatial Reference in fullExtent");
550 : }
551 9 : if (!poSRS->IsSame(&oSRS))
552 : {
553 0 : CPLError(CE_Warning, CPLE_AppDefined,
554 : "Ignoring fullExtent, because its SRS is "
555 : "different from the main one");
556 0 : m_sFullExtent = OGREnvelope();
557 : }
558 : }
559 : }
560 :
561 9 : nBands = EQUAL(compression, "JPEG") ? 3 : 4;
562 45 : for (int i = 1; i <= nBands; i++)
563 : {
564 36 : ECBand *band = new ECBand(this, i);
565 36 : SetBand(i, band);
566 : }
567 : // Keep 4 bundle files open
568 9 : bundles.resize(4);
569 : }
570 4 : catch (CPLString &err)
571 : {
572 2 : error = CE_Failure;
573 2 : CPLError(error, CPLE_OpenFailed, "%s", err.c_str());
574 : }
575 11 : return error;
576 : }
577 :
578 : class ESRICProxyRasterBand final : public GDALProxyRasterBand
579 : {
580 : private:
581 : GDALRasterBand *m_poUnderlyingBand = nullptr;
582 :
583 : CPL_DISALLOW_COPY_ASSIGN(ESRICProxyRasterBand)
584 :
585 : protected:
586 : GDALRasterBand *RefUnderlyingRasterBand(bool /*bForceOpen*/) const override;
587 :
588 : public:
589 20 : explicit ESRICProxyRasterBand(GDALRasterBand *poUnderlyingBand)
590 20 : : m_poUnderlyingBand(poUnderlyingBand)
591 : {
592 20 : nBand = poUnderlyingBand->GetBand();
593 20 : eDataType = poUnderlyingBand->GetRasterDataType();
594 20 : nRasterXSize = poUnderlyingBand->GetXSize();
595 20 : nRasterYSize = poUnderlyingBand->GetYSize();
596 20 : poUnderlyingBand->GetBlockSize(&nBlockXSize, &nBlockYSize);
597 20 : }
598 : };
599 :
600 : GDALRasterBand *
601 28 : ESRICProxyRasterBand::RefUnderlyingRasterBand(bool /*bForceOpen*/) const
602 : {
603 28 : return m_poUnderlyingBand;
604 : }
605 :
606 : class ESRICProxyDataset final : public GDALProxyDataset
607 : {
608 : private:
609 : // m_poSrcDS must be placed before m_poUnderlyingDS for proper destruction
610 : // as m_poUnderlyingDS references m_poSrcDS
611 : std::unique_ptr<GDALDataset> m_poSrcDS{};
612 : std::unique_ptr<GDALDataset> m_poUnderlyingDS{};
613 : CPLStringList m_aosFileList{};
614 :
615 : protected:
616 : GDALDataset *RefUnderlyingDataset() const override;
617 :
618 : public:
619 5 : ESRICProxyDataset(GDALDataset *poSrcDS, GDALDataset *poUnderlyingDS,
620 : const char *pszDescription)
621 5 : : m_poSrcDS(poSrcDS), m_poUnderlyingDS(poUnderlyingDS)
622 : {
623 5 : nRasterXSize = poUnderlyingDS->GetRasterXSize();
624 5 : nRasterYSize = poUnderlyingDS->GetRasterYSize();
625 25 : for (int i = 0; i < poUnderlyingDS->GetRasterCount(); ++i)
626 20 : SetBand(i + 1, new ESRICProxyRasterBand(
627 20 : poUnderlyingDS->GetRasterBand(i + 1)));
628 5 : m_aosFileList.AddString(pszDescription);
629 5 : }
630 :
631 3 : GDALDriver *GetDriver() override
632 : {
633 3 : return GDALDriver::FromHandle(GDALGetDriverByName("ESRIC"));
634 : }
635 :
636 3 : char **GetFileList() override
637 : {
638 3 : return CSLDuplicate(m_aosFileList.List());
639 : }
640 : };
641 :
642 12 : GDALDataset *ESRICProxyDataset::RefUnderlyingDataset() const
643 : {
644 12 : return m_poUnderlyingDS.get();
645 : }
646 :
647 17 : GDALDataset *ECDataset::Open(GDALOpenInfo *poOpenInfo)
648 : {
649 17 : return Open(poOpenInfo, poOpenInfo->pszFilename);
650 : }
651 :
652 24 : GDALDataset *ECDataset::Open(GDALOpenInfo *poOpenInfo,
653 : const char *pszDescription)
654 : {
655 24 : bool ignoreOversizedLods = CSLFetchBoolean(poOpenInfo->papszOpenOptions,
656 24 : "IGNORE_OVERSIZED_LODS", FALSE);
657 24 : if (IdentifyXML(poOpenInfo))
658 : {
659 6 : CPLXMLNode *config = CPLParseXMLFile(poOpenInfo->pszFilename);
660 6 : if (!config) // Error was reported from parsing XML
661 0 : return nullptr;
662 6 : CPLXMLNode *CacheInfo = CPLGetXMLNode(config, "=CacheInfo");
663 6 : if (!CacheInfo)
664 : {
665 0 : CPLError(
666 : CE_Warning, CPLE_OpenFailed,
667 : "Error parsing configuration, can't find CacheInfo element");
668 0 : CPLDestroyXMLNode(config);
669 0 : return nullptr;
670 : }
671 6 : auto ds = new ECDataset();
672 6 : ds->dname = CPLGetDirnameSafe(poOpenInfo->pszFilename) + "/_alllayers";
673 6 : CPLErr error = ds->Initialize(CacheInfo, ignoreOversizedLods);
674 6 : CPLDestroyXMLNode(config);
675 6 : if (CE_None != error)
676 : {
677 2 : delete ds;
678 2 : ds = nullptr;
679 : }
680 6 : return ds;
681 : }
682 18 : else if (IdentifyJSON(poOpenInfo))
683 : {
684 : // Recognize .tpkx file directly passed
685 18 : if (!STARTS_WITH(poOpenInfo->pszFilename, "/vsizip/") &&
686 : #if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
687 11 : ENDS_WITH_CI(poOpenInfo->pszFilename, ".tpkx") &&
688 : #endif
689 7 : memcmp(poOpenInfo->pabyHeader, "PK\x03\x04", 4) == 0)
690 : {
691 14 : GDALOpenInfo oOpenInfo((std::string("/vsizip/{") +
692 21 : poOpenInfo->pszFilename + "}/root.json")
693 : .c_str(),
694 14 : GA_ReadOnly);
695 7 : oOpenInfo.papszOpenOptions = poOpenInfo->papszOpenOptions;
696 7 : return Open(&oOpenInfo, pszDescription);
697 : }
698 :
699 22 : CPLJSONDocument oJSONDocument;
700 11 : if (!oJSONDocument.Load(poOpenInfo->pszFilename))
701 : {
702 0 : CPLError(CE_Warning, CPLE_OpenFailed,
703 : "Error parsing configuration");
704 0 : return nullptr;
705 : }
706 :
707 22 : const CPLJSONObject &oRoot = oJSONDocument.GetRoot();
708 11 : if (!oRoot.IsValid())
709 : {
710 0 : CPLError(CE_Warning, CPLE_OpenFailed, "Invalid json document root");
711 0 : return nullptr;
712 : }
713 :
714 22 : auto ds = std::make_unique<ECDataset>();
715 33 : auto tileBundlesPath = oRoot.GetString("tileBundlesPath");
716 : // Strip leading relative path indicator (if present)
717 11 : if (tileBundlesPath.substr(0, 2) == "./")
718 : {
719 11 : tileBundlesPath.erase(0, 2);
720 : }
721 :
722 11 : ds->dname.Printf("%s/%s",
723 22 : CPLGetDirnameSafe(poOpenInfo->pszFilename).c_str(),
724 22 : tileBundlesPath.c_str());
725 11 : CPLErr error = ds->InitializeFromJSON(oRoot, ignoreOversizedLods);
726 11 : if (CE_None != error)
727 : {
728 2 : return nullptr;
729 : }
730 :
731 : const bool bIsFullExtentValid =
732 9 : (ds->m_sFullExtent.IsInit() &&
733 17 : ds->m_sFullExtent.MinX < ds->m_sFullExtent.MaxX &&
734 8 : ds->m_sFullExtent.MinY < ds->m_sFullExtent.MaxY);
735 : const char *pszExtentSource =
736 9 : CSLFetchNameValue(poOpenInfo->papszOpenOptions, "EXTENT_SOURCE");
737 :
738 18 : CPLStringList aosOptions;
739 9 : if ((!pszExtentSource && bIsFullExtentValid) ||
740 5 : (pszExtentSource && EQUAL(pszExtentSource, "FULL_EXTENT")))
741 : {
742 4 : if (!bIsFullExtentValid)
743 : {
744 0 : CPLError(CE_Failure, CPLE_AppDefined,
745 : "fullExtent is not valid");
746 0 : return nullptr;
747 : }
748 4 : aosOptions.AddString("-projwin");
749 4 : aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MinX));
750 4 : aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MaxY));
751 4 : aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MaxX));
752 4 : aosOptions.AddString(CPLSPrintf("%.17g", ds->m_sFullExtent.MinY));
753 : }
754 5 : else if (pszExtentSource && EQUAL(pszExtentSource, "INITIAL_EXTENT"))
755 : {
756 : const bool bIsInitialExtentValid =
757 1 : (ds->m_sInitialExtent.IsInit() &&
758 2 : ds->m_sInitialExtent.MinX < ds->m_sInitialExtent.MaxX &&
759 1 : ds->m_sInitialExtent.MinY < ds->m_sInitialExtent.MaxY);
760 1 : if (!bIsInitialExtentValid)
761 : {
762 0 : CPLError(CE_Failure, CPLE_AppDefined,
763 : "initialExtent is not valid");
764 0 : return nullptr;
765 : }
766 1 : aosOptions.AddString("-projwin");
767 : aosOptions.AddString(
768 1 : CPLSPrintf("%.17g", ds->m_sInitialExtent.MinX));
769 : aosOptions.AddString(
770 1 : CPLSPrintf("%.17g", ds->m_sInitialExtent.MaxY));
771 : aosOptions.AddString(
772 1 : CPLSPrintf("%.17g", ds->m_sInitialExtent.MaxX));
773 : aosOptions.AddString(
774 1 : CPLSPrintf("%.17g", ds->m_sInitialExtent.MinY));
775 : }
776 :
777 9 : if (!aosOptions.empty())
778 : {
779 5 : aosOptions.AddString("-of");
780 5 : aosOptions.AddString("VRT");
781 5 : aosOptions.AddString("-co");
782 5 : aosOptions.AddString(CPLSPrintf("BLOCKXSIZE=%d", ds->TSZ));
783 5 : aosOptions.AddString("-co");
784 5 : aosOptions.AddString(CPLSPrintf("BLOCKYSIZE=%d", ds->TSZ));
785 : auto psOptions =
786 5 : GDALTranslateOptionsNew(aosOptions.List(), nullptr);
787 5 : auto hDS = GDALTranslate("", GDALDataset::ToHandle(ds.get()),
788 : psOptions, nullptr);
789 5 : GDALTranslateOptionsFree(psOptions);
790 5 : if (!hDS)
791 : {
792 0 : return nullptr;
793 : }
794 : return new ESRICProxyDataset(
795 5 : ds.release(), GDALDataset::FromHandle(hDS), pszDescription);
796 : }
797 4 : return ds.release();
798 : }
799 0 : return nullptr;
800 : }
801 :
802 : // Fetch a reference to an initialized bundle, based on file name
803 : // The returned bundle could still have an invalid file handle, if the
804 : // target bundle is not valid
805 4229 : Bundle &ECDataset::GetBundle(const char *fname)
806 : {
807 4261 : for (auto &bundle : bundles)
808 : {
809 : // If a bundle is missing, it still occupies a slot, with fh == nullptr
810 4253 : if (EQUAL(bundle.name.c_str(), fname))
811 4221 : return bundle;
812 : }
813 : // Not found, look for an empty // missing slot
814 8 : for (auto &bundle : bundles)
815 : {
816 8 : if (nullptr == bundle.fh)
817 : {
818 8 : bundle.Init(fname);
819 8 : return bundle;
820 : }
821 : }
822 : // No empties, eject one
823 : Bundle &bundle = bundles[
824 : #ifndef __COVERITY__
825 0 : rand() % bundles.size()
826 : #else
827 : 0
828 : #endif
829 0 : ];
830 0 : bundle.Init(fname);
831 0 : return bundle;
832 : }
833 :
834 824 : ECBand::~ECBand()
835 : {
836 772 : for (auto ovr : overviews)
837 360 : if (ovr)
838 360 : delete ovr;
839 412 : overviews.clear();
840 824 : }
841 :
842 412 : ECBand::ECBand(ECDataset *parent, int b, int level)
843 412 : : lvl(level), ci(GCI_Undefined)
844 : {
845 : static const GDALColorInterp rgba[4] = {GCI_RedBand, GCI_GreenBand,
846 : GCI_BlueBand, GCI_AlphaBand};
847 : static const GDALColorInterp la[2] = {GCI_GrayIndex, GCI_AlphaBand};
848 412 : poDS = parent;
849 412 : nBand = b;
850 :
851 412 : double factor = parent->resolutions[0] / parent->resolutions[lvl];
852 412 : nRasterXSize = static_cast<int>(parent->nRasterXSize * factor + 0.5);
853 412 : nRasterYSize = static_cast<int>(parent->nRasterYSize * factor + 0.5);
854 412 : nBlockXSize = nBlockYSize = parent->TSZ;
855 :
856 : // Default color interpretation
857 412 : assert(b - 1 >= 0);
858 412 : if (parent->nBands >= 3)
859 : {
860 412 : assert(b - 1 < static_cast<int>(CPL_ARRAYSIZE(rgba)));
861 412 : ci = rgba[b - 1];
862 : }
863 : else
864 : {
865 0 : assert(b - 1 < static_cast<int>(CPL_ARRAYSIZE(la)));
866 0 : ci = la[b - 1];
867 : }
868 412 : if (0 == lvl)
869 52 : AddOverviews();
870 412 : }
871 :
872 52 : void ECBand::AddOverviews()
873 : {
874 52 : auto parent = cpl::down_cast<ECDataset *>(poDS);
875 412 : for (size_t i = 1; i < parent->resolutions.size(); i++)
876 : {
877 360 : ECBand *ovl = new ECBand(parent, nBand, int(i));
878 360 : if (!ovl)
879 0 : break;
880 360 : overviews.push_back(ovl);
881 : }
882 52 : }
883 :
884 4229 : CPLErr ECBand::IReadBlock(int nBlockXOff, int nBlockYOff, void *pData)
885 : {
886 4229 : auto parent = cpl::down_cast<ECDataset *>(poDS);
887 4229 : auto &buffer = parent->tilebuffer;
888 4229 : auto TSZ = parent->TSZ;
889 4229 : auto BSZ = parent->BSZ;
890 4229 : size_t nBytes = size_t(TSZ) * TSZ;
891 :
892 4229 : buffer.resize(nBytes * parent->nBands);
893 :
894 8458 : const int lxx = parent->m_nMinLOD +
895 4229 : static_cast<int>(parent->resolutions.size() - lvl - 1);
896 : int bx, by;
897 4229 : bx = (nBlockXOff / BSZ) * BSZ;
898 4229 : by = (nBlockYOff / BSZ) * BSZ;
899 8458 : CPLString fname;
900 8458 : fname = CPLString().Printf("%s/L%02d/R%04xC%04x.bundle",
901 4229 : parent->dname.c_str(), lxx, by, bx);
902 4229 : Bundle &bundle = parent->GetBundle(fname);
903 4229 : if (nullptr == bundle.fh)
904 : { // This is not an error in general, bundles can be missing
905 64 : CPLDebug("ESRIC", "Can't open bundle %s", fname.c_str());
906 64 : memset(pData, 0, nBytes);
907 64 : return CE_None;
908 : }
909 4165 : int block = static_cast<int>((nBlockYOff % BSZ) * BSZ + (nBlockXOff % BSZ));
910 4165 : GUInt64 offset = bundle.index[block] & 0xffffffffffull;
911 4165 : GUInt64 size = bundle.index[block] >> 40;
912 4165 : if (0 == size)
913 : {
914 3755 : memset(pData, 0, nBytes);
915 3755 : return CE_None;
916 : }
917 410 : auto &fbuffer = parent->filebuffer;
918 410 : fbuffer.resize(size_t(size));
919 410 : bundle.fh->Seek(offset, SEEK_SET);
920 410 : if (size != bundle.fh->Read(fbuffer.data(), size_t(1), size_t(size)))
921 : {
922 0 : CPLError(CE_Failure, CPLE_FileIO,
923 : "Error reading tile, reading " CPL_FRMT_GUIB
924 : " at " CPL_FRMT_GUIB,
925 : GUInt64(size), GUInt64(offset));
926 0 : return CE_Failure;
927 : }
928 820 : const CPLString magic(VSIMemGenerateHiddenFilename("esric.tmp"));
929 410 : auto mfh = VSIFileFromMemBuffer(magic.c_str(), fbuffer.data(), size, false);
930 410 : VSIFCloseL(mfh);
931 : // Can't open a raster by handle?
932 410 : auto inds = GDALOpen(magic.c_str(), GA_ReadOnly);
933 410 : if (!inds)
934 : {
935 0 : VSIUnlink(magic.c_str());
936 0 : CPLError(CE_Failure, CPLE_FileIO, "Error opening tile");
937 0 : return CE_Failure;
938 : }
939 : // Duplicate first band if not sufficient bands are provided
940 410 : auto inbands = GDALGetRasterCount(inds);
941 410 : int ubands[4] = {1, 1, 1, 1};
942 410 : int *usebands = nullptr;
943 410 : int bandcount = parent->nBands;
944 410 : GDALColorTableH hCT = nullptr;
945 410 : if (inbands != bandcount)
946 : {
947 : // Opaque if output expects alpha channel
948 259 : if (0 == bandcount % 2)
949 : {
950 259 : fill(buffer.begin(), buffer.end(), GByte(255));
951 259 : bandcount--;
952 : }
953 259 : if (3 == inbands)
954 : {
955 : // Lacking opacity, copy the first three bands
956 8 : ubands[1] = 2;
957 8 : ubands[2] = 3;
958 8 : usebands = ubands;
959 : }
960 251 : else if (1 == inbands)
961 : {
962 : // Grayscale, expecting color
963 251 : usebands = ubands;
964 : // Check for the color table of 1 band rasters
965 251 : hCT = GDALGetRasterColorTable(GDALGetRasterBand(inds, 1));
966 : }
967 : }
968 :
969 410 : auto errcode = CE_None;
970 410 : if (nullptr != hCT)
971 : {
972 : // Expand color indexed to RGB(A)
973 250 : errcode = GDALDatasetRasterIO(
974 250 : inds, GF_Read, 0, 0, TSZ, TSZ, buffer.data(), TSZ, TSZ, GDT_UInt8,
975 250 : 1, usebands, parent->nBands, parent->nBands * TSZ, 1);
976 250 : if (CE_None == errcode)
977 : {
978 : GByte abyCT[4 * 256];
979 250 : GByte *pabyTileData = buffer.data();
980 250 : const int nEntries = std::min(256, GDALGetColorEntryCount(hCT));
981 2148 : for (int i = 0; i < nEntries; i++)
982 : {
983 1898 : const GDALColorEntry *psEntry = GDALGetColorEntry(hCT, i);
984 1898 : abyCT[4 * i] = static_cast<GByte>(psEntry->c1);
985 1898 : abyCT[4 * i + 1] = static_cast<GByte>(psEntry->c2);
986 1898 : abyCT[4 * i + 2] = static_cast<GByte>(psEntry->c3);
987 1898 : abyCT[4 * i + 3] = static_cast<GByte>(psEntry->c4);
988 : }
989 62352 : for (int i = nEntries; i < 256; i++)
990 : {
991 62102 : abyCT[4 * i] = 0;
992 62102 : abyCT[4 * i + 1] = 0;
993 62102 : abyCT[4 * i + 2] = 0;
994 62102 : abyCT[4 * i + 3] = 0;
995 : }
996 :
997 250 : if (parent->nBands == 4)
998 : {
999 16384200 : for (size_t i = 0; i < nBytes; i++)
1000 : {
1001 16384000 : const GByte byVal = pabyTileData[4 * i];
1002 16384000 : pabyTileData[4 * i] = abyCT[4 * byVal];
1003 16384000 : pabyTileData[4 * i + 1] = abyCT[4 * byVal + 1];
1004 16384000 : pabyTileData[4 * i + 2] = abyCT[4 * byVal + 2];
1005 16384000 : pabyTileData[4 * i + 3] = abyCT[4 * byVal + 3];
1006 : }
1007 : }
1008 0 : else if (parent->nBands == 3)
1009 : {
1010 0 : for (size_t i = 0; i < nBytes; i++)
1011 : {
1012 0 : const GByte byVal = pabyTileData[3 * i];
1013 0 : pabyTileData[3 * i] = abyCT[4 * byVal];
1014 0 : pabyTileData[3 * i + 1] = abyCT[4 * byVal + 1];
1015 0 : pabyTileData[3 * i + 2] = abyCT[4 * byVal + 2];
1016 : }
1017 : }
1018 : else
1019 : {
1020 : // Assuming grayscale output
1021 0 : for (size_t i = 0; i < nBytes; i++)
1022 : {
1023 0 : const GByte byVal = pabyTileData[i];
1024 0 : pabyTileData[i] = abyCT[4 * byVal];
1025 : }
1026 : }
1027 : }
1028 : }
1029 : else
1030 : {
1031 160 : errcode = GDALDatasetRasterIO(
1032 160 : inds, GF_Read, 0, 0, TSZ, TSZ, buffer.data(), TSZ, TSZ, GDT_UInt8,
1033 160 : bandcount, usebands, parent->nBands, parent->nBands * TSZ, 1);
1034 : }
1035 410 : GDALClose(inds);
1036 410 : VSIUnlink(magic.c_str());
1037 : // Error while unpacking tile
1038 410 : if (CE_None != errcode)
1039 0 : return errcode;
1040 :
1041 2050 : for (int iBand = 1; iBand <= parent->nBands; iBand++)
1042 : {
1043 1640 : auto band = parent->GetRasterBand(iBand);
1044 1640 : if (lvl)
1045 52 : band = band->GetOverview(lvl - 1);
1046 1640 : GDALRasterBlock *poBlock = nullptr;
1047 1640 : if (band != this)
1048 : {
1049 1230 : poBlock = band->GetLockedBlockRef(nBlockXOff, nBlockYOff, 1);
1050 1230 : if (poBlock != nullptr)
1051 : {
1052 1230 : GDALCopyWords(buffer.data() + iBand - 1, GDT_UInt8,
1053 : parent->nBands, poBlock->GetDataRef(), GDT_UInt8,
1054 : 1, TSZ * TSZ);
1055 1230 : poBlock->DropLock();
1056 : }
1057 : }
1058 : else
1059 : {
1060 410 : GDALCopyWords(buffer.data() + iBand - 1, GDT_UInt8, parent->nBands,
1061 : pData, GDT_UInt8, 1, TSZ * TSZ);
1062 : }
1063 : }
1064 :
1065 410 : return CE_None;
1066 : } // IReadBlock
1067 :
1068 : } // namespace ESRIC
1069 :
1070 2058 : void CPL_DLL GDALRegister_ESRIC()
1071 : {
1072 2058 : if (GDALGetDriverByName("ESRIC") != nullptr)
1073 283 : return;
1074 :
1075 1775 : auto poDriver = new GDALDriver;
1076 :
1077 1775 : poDriver->SetDescription("ESRIC");
1078 1775 : poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
1079 1775 : poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
1080 1775 : poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "Esri Compact Cache");
1081 :
1082 1775 : poDriver->SetMetadataItem(GDAL_DMD_EXTENSIONS, "json tpkx");
1083 :
1084 1775 : poDriver->SetMetadataItem(
1085 : GDAL_DMD_OPENOPTIONLIST,
1086 : "<OpenOptionList>"
1087 : " <Option name='EXTENT_SOURCE' type='string-select' "
1088 : "description='Which source is used to determine the extent' "
1089 : "default='FULL_EXTENT'>"
1090 : " <Value>FULL_EXTENT</Value>"
1091 : " <Value>INITIAL_EXTENT</Value>"
1092 : " <Value>TILING_SCHEME</Value>"
1093 : " </Option>"
1094 : " <Option name='IGNORE_OVERSIZED_LODS' type='boolean' "
1095 : "description='Whether to silently ignore LODs that exceed the "
1096 : "maximum size supported by GDAL (INT32_MAX)' "
1097 : "default='NO'>"
1098 : " </Option>"
1099 1775 : "</OpenOptionList>");
1100 1775 : poDriver->pfnIdentify = ESRIC::Identify;
1101 1775 : poDriver->pfnOpen = ESRIC::ECDataset::Open;
1102 1775 : poDriver->pfnDelete = ESRIC::Delete;
1103 :
1104 1775 : GetGDALDriverManager()->RegisterDriver(poDriver);
1105 : }
|