LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/pmtiles - ogrpmtilesfromtileset.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 212 258 82.2 %
Date: 2026-05-29 23:25:07 Functions: 4 5 80.0 %

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

Generated by: LCOV version 1.14