Coverage Report

Created: 2026-08-14 09:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/ogr/ogrsf_frmts/pmtiles/ogrpmtilesfromtileset.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  OpenGIS Simple Features Reference Implementation
4
 * Purpose:  Implementation of PMTiles
5
 * Author:   Even Rouault <even.rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2026, Even Rouault <even.rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "ogrpmtilesfromtileset.h"
14
#include "ogr_pmtiles.h"
15
16
#include "cpl_json.h"
17
#include "cpl_md5.h"
18
#include "cpl_vsi_virtual.h"
19
#include "gdal_priv.h"
20
21
#include "include_pmtiles.h"
22
23
#include <algorithm>
24
#include <array>
25
#include <cassert>
26
#include <map>
27
#include <unordered_map>
28
#include <unordered_set>
29
#include <utility>
30
31
/************************************************************************/
32
/*            OGRPMTilesConvertFromTilesetInitializeHeader()            */
33
/************************************************************************/
34
35
static void OGRPMTilesConvertFromTilesetInitializeHeader(
36
    GDALDataset *poSrcDS, CSLConstList papszOptions, int nMinZoom, int nMaxZoom,
37
    pmtiles::headerv3 &sHeader, const char *&pszExt, std::string &osMetadata)
38
0
{
39
0
    OGREnvelope sExtent;
40
0
    poSrcDS->GetExtentWGS84LongLat(&sExtent);
41
42
0
    constexpr double MAX_LAT = 85.0511287798066;
43
0
    sExtent.MinY = std::max(-MAX_LAT, sExtent.MinY);
44
0
    sExtent.MaxY = std::min(MAX_LAT, sExtent.MaxY);
45
46
0
    CPLJSONObject oObj;
47
0
    const char *pszTileFormat =
48
0
        CSLFetchNameValueDef(papszOptions, "TILE_FORMAT", "PNG");
49
0
    const char *pszVersion = CSLFetchNameValueDef(
50
0
        papszOptions, "VERSION", EQUAL(pszTileFormat, "WEBP") ? "1.3" : "1.1");
51
0
    oObj.Set("version", pszVersion);
52
0
    oObj.Set("name", CSLFetchNameValueDef(papszOptions, "NAME", ""));
53
0
    oObj.Set("description",
54
0
             CSLFetchNameValueDef(papszOptions, "DESCRIPTION", ""));
55
0
    oObj.Set("type", CSLFetchNameValueDef(papszOptions, "TYPE", "overlay"));
56
0
    if (const char *pszElevationType =
57
0
            CSLFetchNameValue(papszOptions, "ELEVATION_TYPE"))
58
0
        oObj.Set("elevation_type", pszElevationType);
59
0
    uint8_t tile_type = pmtiles::TILETYPE_UNKNOWN;
60
0
    if (EQUAL(pszTileFormat, "PNG"))
61
0
    {
62
0
        pszExt = "png";
63
0
        tile_type = pmtiles::TILETYPE_PNG;
64
0
    }
65
0
    else if (EQUAL(pszTileFormat, "JPEG"))
66
0
    {
67
0
        pszExt = "jpg";
68
0
        tile_type = pmtiles::TILETYPE_JPEG;
69
0
    }
70
0
    else if (EQUAL(pszTileFormat, "WEBP"))
71
0
    {
72
0
        pszExt = "webp";
73
0
        tile_type = pmtiles::TILETYPE_WEBP;
74
0
    }
75
0
    else
76
0
        CPLAssert(false);
77
78
0
    oObj.Set("format", pszExt);
79
0
    oObj.Set("scheme", "xyz");
80
0
    oObj.Set("bounds", CPLSPrintf("%.17g,%.17g,%.17g,%.17g", sExtent.MinX,
81
0
                                  sExtent.MinY, sExtent.MaxX, sExtent.MaxY));
82
0
    const double dfCenterLong = (sExtent.MinX + sExtent.MaxX) / 2;
83
0
    const double dfCenterLat = (sExtent.MinY + sExtent.MaxY) / 2;
84
0
    const int nCenterZoom = nMaxZoom;
85
0
    oObj.Set("center", CPLSPrintf("%.17g,%.17g,%d", dfCenterLong, dfCenterLat,
86
0
                                  nCenterZoom));
87
0
    oObj.Set("minzoom", CPLSPrintf("%d", nMinZoom));
88
0
    oObj.Set("maxzoom", CPLSPrintf("%d", nMaxZoom));
89
90
0
    CPLJSONDocument oMetadataDoc;
91
0
    oMetadataDoc.SetRoot(oObj);
92
0
    osMetadata = oMetadataDoc.SaveAsString();
93
    // CPLDebugOnly("PMTiles", "Metadata = %s", osMetadata.c_str());
94
95
0
    sHeader.root_dir_offset = PMTILES_HEADER_LENGTH;
96
0
    sHeader.root_dir_bytes = 0;
97
0
    sHeader.json_metadata_offset = 0;
98
0
    sHeader.json_metadata_bytes = 0;
99
0
    sHeader.leaf_dirs_offset = 0;
100
0
    sHeader.leaf_dirs_bytes = 0;
101
0
    sHeader.tile_data_offset = 0;
102
0
    sHeader.tile_data_bytes = 0;
103
0
    sHeader.addressed_tiles_count = 0;
104
0
    sHeader.tile_entries_count = 0;
105
0
    sHeader.tile_contents_count = 0;
106
0
    sHeader.clustered = true;
107
0
    sHeader.internal_compression = pmtiles::COMPRESSION_GZIP;
108
0
    sHeader.tile_compression = pmtiles::COMPRESSION_NONE;
109
0
    sHeader.tile_type = tile_type;
110
0
    sHeader.min_zoom = static_cast<uint8_t>(nMinZoom);
111
0
    sHeader.max_zoom = static_cast<uint8_t>(nMaxZoom);
112
0
    sHeader.min_lon_e7 = static_cast<int32_t>(sExtent.MinX * 10e6);
113
0
    sHeader.min_lat_e7 = static_cast<int32_t>(sExtent.MinY * 10e6);
114
0
    sHeader.max_lon_e7 = static_cast<int32_t>(sExtent.MaxX * 10e6);
115
0
    sHeader.max_lat_e7 = static_cast<int32_t>(sExtent.MaxY * 10e6);
116
0
    sHeader.center_zoom = static_cast<uint8_t>(nCenterZoom);
117
0
    sHeader.center_lon_e7 = static_cast<int32_t>(dfCenterLong * 10e6);
118
0
    sHeader.center_lat_e7 = static_cast<int32_t>(dfCenterLat * 10e6);
119
0
}
120
121
/************************************************************************/
122
/*                    OGRPMTilesConvertFromTileset()                    */
123
/************************************************************************/
124
125
bool OGRPMTilesConvertFromTileset(const char *pszDestName,
126
                                  const char *pszSrcDirectory,
127
                                  GDALDataset *poSrcDS,
128
                                  CSLConstList papszOptions)
129
0
{
130
0
    const CPLStringList aosZoomLevelDirs(VSIReadDir(pszSrcDirectory));
131
0
    int nMinZoom = INT_MAX;
132
0
    int nMaxZoom = 0;
133
0
    for (const char *pszName : cpl::Iterate(aosZoomLevelDirs))
134
0
    {
135
0
        if (CPLGetValueType(pszName) == CPL_VALUE_INTEGER)
136
0
        {
137
0
            const int nZoom = atoi(pszName);
138
0
            nMinZoom = std::min(nMinZoom, nZoom);
139
0
            nMaxZoom = std::max(nMaxZoom, nZoom);
140
0
        }
141
0
    }
142
0
    if (nMinZoom > nMaxZoom)
143
0
    {
144
0
        CPLError(CE_Failure, CPLE_AppDefined, "No valid tile found");
145
0
        return false;
146
0
    }
147
148
0
    pmtiles::headerv3 sHeader;
149
0
    const char *pszExt = "";
150
0
    std::string osMetadata;
151
0
    OGRPMTilesConvertFromTilesetInitializeHeader(
152
0
        poSrcDS, papszOptions, nMinZoom, nMaxZoom, sHeader, pszExt, osMetadata);
153
154
0
    struct TileEntry
155
0
    {
156
0
        uint64_t nTileId;
157
0
        std::array<unsigned char, 16> abyMD5;
158
0
    };
159
160
    // In a first step browse through the tiles table to compute the PMTiles
161
    // tile_id of each tile, and compute a hash of the tile data for
162
    // deduplication
163
0
    std::vector<TileEntry> asTileEntries;
164
0
    std::vector<GByte> abyTileData;
165
0
    std::map<uint64_t, uint32_t> oMapTileIdToFileSize;
166
0
    for (int nZoom = nMinZoom; nZoom <= nMaxZoom; ++nZoom)
167
0
    {
168
0
        const std::string osZoomDir(CPLFormFilenameSafe(
169
0
            pszSrcDirectory, CPLSPrintf("%d", nZoom), nullptr));
170
0
        std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> psDirX(
171
0
            VSIOpenDir(osZoomDir.c_str(), 0, nullptr), VSICloseDir);
172
0
        if (!psDirX)
173
0
            return false;
174
0
        while (const VSIDIREntry *psXEntry = VSIGetNextDirEntry(psDirX.get()))
175
0
        {
176
0
            const std::string osXDir(CPLFormFilenameSafe(
177
0
                osZoomDir.c_str(), psXEntry->pszName, nullptr));
178
0
            const int nX = atoi(psXEntry->pszName);
179
0
            std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> psDirY(
180
0
                VSIOpenDir(osXDir.c_str(), 0, nullptr), VSICloseDir);
181
0
            if (!psDirY)
182
0
                return false;
183
0
            while (const VSIDIREntry *psYEntry =
184
0
                       VSIGetNextDirEntry(psDirY.get()))
185
0
            {
186
0
                const int nY = atoi(psYEntry->pszName);
187
0
                uint64_t nTileId;
188
0
                try
189
0
                {
190
0
                    nTileId = pmtiles::zxy_to_tileid(
191
0
                        static_cast<uint8_t>(nZoom), nX, nY);
192
0
                }
193
0
                catch (const std::exception &e)
194
0
                {
195
0
                    CPLError(CE_Failure, CPLE_AppDefined,
196
0
                             "Cannot compute tile id: %s", e.what());
197
0
                    return false;
198
0
                }
199
200
0
                const std::string osTileFilename(CPLFormFilenameSafe(
201
0
                    osXDir.c_str(), psYEntry->pszName, nullptr));
202
203
0
                VSIStatBufL sStatBuf;
204
0
                if (VSIStatL(osTileFilename.c_str(), &sStatBuf) != 0)
205
0
                {
206
0
                    CPLError(CE_Failure, CPLE_AppDefined,
207
0
                             "Cannot stat file: %s", osTileFilename.c_str());
208
0
                    return false;
209
0
                }
210
211
                // Arbitrary (but must not be larger than UINT32_MAX per PMTiles spec)
212
0
                constexpr uint32_t MAX_TILE_SIZE = 100 * 1024 * 1024;
213
0
                if (sStatBuf.st_size > MAX_TILE_SIZE)
214
0
                {
215
0
                    CPLError(CE_Failure, CPLE_AppDefined, "Too large file: %s",
216
0
                             osTileFilename.c_str());
217
0
                    return false;
218
0
                }
219
220
0
                const uint32_t nFileSize =
221
0
                    static_cast<uint32_t>(sStatBuf.st_size);
222
223
0
                if (abyTileData.size() < nFileSize)
224
0
                    abyTileData.resize(nFileSize);
225
226
0
                auto fp = VSIVirtualHandleUniquePtr(
227
0
                    VSIFOpenL(osTileFilename.c_str(), "rb"));
228
0
                if (!fp)
229
0
                {
230
0
                    CPLError(CE_Failure, CPLE_AppDefined, "Cannot open %s",
231
0
                             osTileFilename.c_str());
232
0
                    return false;
233
0
                }
234
0
                if (fp->Read(abyTileData.data(), nFileSize) != nFileSize)
235
0
                {
236
0
                    return false;
237
0
                }
238
239
0
                oMapTileIdToFileSize[nTileId] = nFileSize;
240
241
0
                TileEntry sEntry;
242
0
                sEntry.nTileId = nTileId;
243
244
0
                CPLMD5Context md5context;
245
0
                CPLMD5Init(&md5context);
246
0
                CPLMD5Update(&md5context, abyTileData.data(), nFileSize);
247
0
                CPLMD5Final(&sEntry.abyMD5[0], &md5context);
248
0
                try
249
0
                {
250
0
                    asTileEntries.push_back(sEntry);
251
0
                }
252
0
                catch (const std::exception &e)
253
0
                {
254
0
                    CPLError(CE_Failure, CPLE_AppDefined,
255
0
                             "Out of memory browsing through tiles: %s",
256
0
                             e.what());
257
0
                    return false;
258
0
                }
259
0
            }
260
0
        }
261
0
    }
262
263
    // Sort the tiles by ascending tile_id. This is a requirement to build
264
    // the PMTiles directories.
265
0
    std::sort(asTileEntries.begin(), asTileEntries.end(),
266
0
              [](const TileEntry &a, const TileEntry &b)
267
0
              { return a.nTileId < b.nTileId; });
268
269
    // Let's gather tile data in
270
    // a way that corresponds to the "clustered" mode, that is
271
    // "offsets are either contiguous with the previous offset+length, or
272
    // refer to a lesser offset, when writing with deduplication."
273
274
0
    std::vector<pmtiles::entryv3> asPMTilesEntries;
275
0
    uint64_t nFileOffset = 0;
276
0
    std::unordered_map<std::array<unsigned char, 16>,
277
0
                       std::pair<uint64_t, uint32_t>,
278
0
                       HashArray<unsigned char, 16>>
279
0
        oMapMD5ToOffsetLen;
280
0
    {
281
0
        uint64_t nLastTileId = 0;
282
0
        std::array<unsigned char, 16> abyLastMD5{0, 0, 0, 0, 0, 0, 0, 0,
283
0
                                                 0, 0, 0, 0, 0, 0, 0, 0};
284
0
        for (const auto &sEntry : asTileEntries)
285
0
        {
286
0
            if (sEntry.nTileId == nLastTileId + 1 &&
287
0
                sEntry.abyMD5 == abyLastMD5)
288
0
            {
289
                // If the tile id immediately follows the previous one and
290
                // has the same tile data, increase the run_length
291
0
                asPMTilesEntries.back().run_length++;
292
0
            }
293
0
            else
294
0
            {
295
0
                pmtiles::entryv3 sPMTilesEntry;
296
0
                sPMTilesEntry.tile_id = sEntry.nTileId;
297
0
                sPMTilesEntry.run_length = 1;
298
299
0
                auto oIter = oMapMD5ToOffsetLen.find(sEntry.abyMD5);
300
0
                if (oIter != oMapMD5ToOffsetLen.end())
301
0
                {
302
                    // Point to previously written tile data if this content
303
                    // has already been written
304
0
                    sPMTilesEntry.offset = oIter->second.first;
305
0
                    sPMTilesEntry.length = oIter->second.second;
306
0
                }
307
0
                else
308
0
                {
309
0
                    const auto oIterToFileSize =
310
0
                        oMapTileIdToFileSize.find(sEntry.nTileId);
311
0
                    CPLAssert(oIterToFileSize != oMapTileIdToFileSize.end());
312
0
                    const uint32_t nTileDataLength = oIterToFileSize->second;
313
314
0
                    sPMTilesEntry.offset = nFileOffset;
315
0
                    sPMTilesEntry.length = nTileDataLength;
316
317
0
                    oMapMD5ToOffsetLen[sEntry.abyMD5] =
318
0
                        std::pair<uint64_t, uint32_t>(nFileOffset,
319
0
                                                      nTileDataLength);
320
321
0
                    nFileOffset += nTileDataLength;
322
0
                }
323
324
0
                asPMTilesEntries.push_back(sPMTilesEntry);
325
326
0
                nLastTileId = sEntry.nTileId;
327
0
                abyLastMD5 = sEntry.abyMD5;
328
0
            }
329
0
        }
330
0
    }
331
332
0
    const CPLCompressor *psCompressor = CPLGetCompressor("gzip");
333
0
    assert(psCompressor);
334
0
    std::string osCompressed;
335
336
0
    struct compression_exception : std::exception
337
0
    {
338
0
        const char *what() const noexcept override
339
0
        {
340
0
            return "Compression failed";
341
0
        }
342
0
    };
343
344
0
    const auto oCompressFunc = [psCompressor,
345
0
                                &osCompressed](const std::string &osBytes,
346
0
                                               uint8_t) -> std::string
347
0
    {
348
0
        osCompressed.resize(32 + osBytes.size() * 2);
349
0
        size_t nOutputSize = osCompressed.size();
350
0
        void *pOutputData = &osCompressed[0];
351
0
        if (!psCompressor->pfnFunc(osBytes.data(), osBytes.size(), &pOutputData,
352
0
                                   &nOutputSize, nullptr,
353
0
                                   psCompressor->user_data))
354
0
        {
355
0
            throw compression_exception();
356
0
        }
357
0
        osCompressed.resize(nOutputSize);
358
0
        return osCompressed;
359
0
    };
360
361
0
    std::string osCompressedMetadata;
362
363
0
    std::string osRootBytes;
364
0
    std::string osLeaveBytes;
365
0
    int nNumLeaves;
366
0
    try
367
0
    {
368
0
        osCompressedMetadata =
369
0
            oCompressFunc(osMetadata, pmtiles::COMPRESSION_GZIP);
370
371
        // Build the root and leave directories (one depth max)
372
0
        std::tie(osRootBytes, osLeaveBytes, nNumLeaves) =
373
0
            pmtiles::make_root_leaves(oCompressFunc, pmtiles::COMPRESSION_GZIP,
374
0
                                      asPMTilesEntries);
375
0
    }
376
0
    catch (const std::exception &e)
377
0
    {
378
0
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot build directories: %s",
379
0
                 e.what());
380
0
        return false;
381
0
    }
382
383
    // Finalize the header fields related to offsets and size of the
384
    // different parts of the file
385
0
    sHeader.root_dir_bytes = osRootBytes.size();
386
0
    sHeader.json_metadata_offset =
387
0
        sHeader.root_dir_offset + sHeader.root_dir_bytes;
388
0
    sHeader.json_metadata_bytes = osCompressedMetadata.size();
389
0
    sHeader.leaf_dirs_offset =
390
0
        sHeader.json_metadata_offset + sHeader.json_metadata_bytes;
391
0
    sHeader.leaf_dirs_bytes = osLeaveBytes.size();
392
0
    sHeader.tile_data_offset =
393
0
        sHeader.leaf_dirs_offset + sHeader.leaf_dirs_bytes;
394
0
    sHeader.tile_data_bytes = nFileOffset;
395
396
    // Number of tiles that are addressable in the PMTiles archive, that is
397
    // the number of tiles we would have if not deduplicating them
398
0
    sHeader.addressed_tiles_count = asTileEntries.size();
399
400
    // Number of tile entries in root and leave directories
401
    // ie entries whose run_length >= 1
402
0
    sHeader.tile_entries_count = asPMTilesEntries.size();
403
404
    // Number of distinct tile blobs
405
0
    sHeader.tile_contents_count = oMapMD5ToOffsetLen.size();
406
407
    // Now build the file!
408
0
    auto poFile = VSIVirtualHandleUniquePtr(VSIFOpenL(pszDestName, "wb"));
409
0
    if (!poFile)
410
0
    {
411
0
        CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s for write",
412
0
                 pszDestName);
413
0
        return false;
414
0
    }
415
0
    const auto osHeader = sHeader.serialize();
416
417
0
    if (poFile->Write(osHeader.data(), osHeader.size(), 1) != 1 ||
418
0
        poFile->Write(osRootBytes.data(), osRootBytes.size(), 1) != 1 ||
419
0
        poFile->Write(osCompressedMetadata.data(), osCompressedMetadata.size(),
420
0
                      1) != 1 ||
421
0
        (!osLeaveBytes.empty() &&
422
0
         poFile->Write(osLeaveBytes.data(), osLeaveBytes.size(), 1) != 1))
423
0
    {
424
0
        CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
425
0
        return false;
426
0
    }
427
428
    // Copy tile content at end of the output file.
429
0
    {
430
0
        uint64_t nLastTileId = 0;
431
0
        uint64_t nFileOffset2 = 0;
432
0
        std::array<unsigned char, 16> abyLastMD5{0, 0, 0, 0, 0, 0, 0, 0,
433
0
                                                 0, 0, 0, 0, 0, 0, 0, 0};
434
0
        std::unordered_set<std::array<unsigned char, 16>,
435
0
                           HashArray<unsigned char, 16>>
436
0
            oSetMD5;
437
0
        for (const auto &sEntry : asTileEntries)
438
0
        {
439
0
            if (sEntry.nTileId == nLastTileId + 1 &&
440
0
                sEntry.abyMD5 == abyLastMD5)
441
0
            {
442
                // If the tile id immediately follows the previous one and
443
                // has the same tile data, do nothing
444
0
            }
445
0
            else
446
0
            {
447
0
                auto oIter = oSetMD5.find(sEntry.abyMD5);
448
0
                if (oIter == oSetMD5.end())
449
0
                {
450
0
                    int nZ, nX, nY;
451
0
                    try
452
0
                    {
453
0
                        const auto sXYZ =
454
0
                            pmtiles::tileid_to_zxy(sEntry.nTileId);
455
0
                        nZ = sXYZ.z;
456
0
                        nY = sXYZ.y;
457
0
                        nX = sXYZ.x;
458
0
                    }
459
0
                    catch (const std::exception &e)
460
0
                    {
461
                        // shouldn't happen given previous checks
462
0
                        CPLError(CE_Failure, CPLE_AppDefined,
463
0
                                 "Cannot compute xyz: %s", e.what());
464
0
                        return false;
465
0
                    }
466
467
0
                    const std::string osZoomDir(CPLFormFilenameSafe(
468
0
                        pszSrcDirectory, CPLSPrintf("%d", nZ), nullptr));
469
0
                    const std::string osXDir(CPLFormFilenameSafe(
470
0
                        osZoomDir.c_str(), CPLSPrintf("%d", nX), nullptr));
471
0
                    const std::string osTileFilename(CPLFormFilenameSafe(
472
0
                        osXDir.c_str(), CPLSPrintf("%d", nY), pszExt));
473
474
0
                    const auto oIterToFileSize =
475
0
                        oMapTileIdToFileSize.find(sEntry.nTileId);
476
0
                    CPLAssert(oIterToFileSize != oMapTileIdToFileSize.end());
477
0
                    const uint32_t nTileDataLength = oIterToFileSize->second;
478
479
0
                    auto fp = VSIVirtualHandleUniquePtr(
480
0
                        VSIFOpenL(osTileFilename.c_str(), "rb"));
481
0
                    if (!fp)
482
0
                    {
483
0
                        CPLError(CE_Failure, CPLE_AppDefined, "Cannot open %s",
484
0
                                 osTileFilename.c_str());
485
0
                        return false;
486
0
                    }
487
0
                    if (fp->Read(abyTileData.data(), nTileDataLength) !=
488
0
                        nTileDataLength)
489
0
                    {
490
0
                        return false;
491
0
                    }
492
493
0
                    oSetMD5.insert(sEntry.abyMD5);
494
495
0
                    if (poFile->Write(abyTileData.data(), nTileDataLength, 1) !=
496
0
                        1)
497
0
                    {
498
0
                        CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
499
0
                        return false;
500
0
                    }
501
502
0
                    nFileOffset2 += nTileDataLength;
503
0
                }
504
505
0
                nLastTileId = sEntry.nTileId;
506
0
                abyLastMD5 = sEntry.abyMD5;
507
0
            }
508
0
        }
509
510
0
        CPL_IGNORE_RET_VAL(nFileOffset2);
511
0
        CPLAssert(nFileOffset2 == nFileOffset);
512
0
    }
513
514
0
    if (poFile->Close() != 0)
515
0
        return false;
516
517
0
    return true;
518
0
}