/src/gdal/frmts/snap_tiff/snaptiffdriver.cpp
Line | Count | Source |
1 | | // SPDX-License-Identifier: MIT |
2 | | // Copyright 2024, Even Rouault <even.rouault at spatialys.com> |
3 | | |
4 | | #include "cpl_port.h" |
5 | | #include "cpl_minixml.h" |
6 | | #include "cpl_vsi_virtual.h" |
7 | | #include "gdal_frmts.h" |
8 | | #include "gdal_pam.h" |
9 | | #include "gdal_driver.h" |
10 | | #include "gdal_drivermanager.h" |
11 | | #include "gdal_openinfo.h" |
12 | | #include "gdal_cpp_functions.h" |
13 | | #include "rawdataset.h" |
14 | | |
15 | 341 | #define LIBERTIFF_NS GDAL_libertiff |
16 | | #include "libertiff.hpp" |
17 | | |
18 | | #include <algorithm> |
19 | | #include <cassert> |
20 | | #include <cmath> |
21 | | |
22 | | constexpr const char *SNAP_TIFF_PREFIX = "SNAP_TIFF:"; |
23 | | |
24 | | // Non-standard TIFF tag holding DIMAP XML for SNAP TIFF products |
25 | | constexpr uint16_t DIMAP_TAG = 65000; |
26 | | |
27 | | // Number of values per GCP in the GeoTIFFTiePoints tag |
28 | | constexpr int VALUES_PER_GCP = 6; |
29 | | |
30 | | constexpr int GCP_PIXEL = 0; |
31 | | constexpr int GCP_LINE = 1; |
32 | | // constexpr int GCP_DEPTH = 2; |
33 | | constexpr int GCP_X = 3; |
34 | | constexpr int GCP_Y = 4; |
35 | | constexpr int GCP_Z = 5; |
36 | | |
37 | | /************************************************************************/ |
38 | | /* SNAPTIFFDataset */ |
39 | | /************************************************************************/ |
40 | | |
41 | | class SNAPTIFFDataset final : public GDALPamDataset |
42 | | { |
43 | | public: |
44 | 0 | SNAPTIFFDataset() = default; |
45 | | |
46 | | static int Identify(GDALOpenInfo *poOpenInfo); |
47 | | static GDALDataset *Open(GDALOpenInfo *poOpenInfo); |
48 | | |
49 | | char **GetMetadataDomainList() override; |
50 | | CSLConstList GetMetadata(const char *pszDomain = "") override; |
51 | | const char *GetMetadataItem(const char *pszName, |
52 | | const char *pszDomain = "") override; |
53 | | |
54 | | const OGRSpatialReference *GetGCPSpatialRef() const override |
55 | 0 | { |
56 | 0 | return m_oSRS.IsEmpty() ? nullptr : &m_oSRS; |
57 | 0 | } |
58 | | |
59 | | int GetGCPCount() override |
60 | 0 | { |
61 | 0 | return int(m_asGCPs.size()); |
62 | 0 | } |
63 | | |
64 | | const GDAL_GCP *GetGCPs() override |
65 | 0 | { |
66 | 0 | return gdal::GCP::c_ptr(m_asGCPs); |
67 | 0 | } |
68 | | |
69 | | private: |
70 | | VSIVirtualHandleUniquePtr m_poFile{}; |
71 | | std::unique_ptr<const LIBERTIFF_NS::Image> m_poImage{}; |
72 | | |
73 | | //! Whether this dataset is actually the geolocation array |
74 | | bool m_bIsGeolocArray = false; |
75 | | |
76 | | //! Content of "xml:DIMAP" metadata domain |
77 | | CPLStringList m_aosDIMAPMetadata{}; |
78 | | |
79 | | CPLStringList m_aosGEOLOCATION{}; |
80 | | int m_nGeolocArrayWidth = 0; |
81 | | int m_nGeolocArrayHeight = 0; |
82 | | |
83 | | CPLStringList m_aosSUBDATASETS{}; |
84 | | |
85 | | std::vector<gdal::GCP> m_asGCPs{}; |
86 | | OGRSpatialReference m_oSRS{}; |
87 | | |
88 | | bool GetGeolocationMetadata(); |
89 | | |
90 | | void ReadSRS(); |
91 | | }; |
92 | | |
93 | | /************************************************************************/ |
94 | | /* Identify() */ |
95 | | /************************************************************************/ |
96 | | |
97 | | int SNAPTIFFDataset::Identify(GDALOpenInfo *poOpenInfo) |
98 | 568k | { |
99 | 568k | if (STARTS_WITH(poOpenInfo->pszFilename, SNAP_TIFF_PREFIX)) |
100 | 0 | return true; |
101 | | |
102 | 568k | if (poOpenInfo->fpL == nullptr || poOpenInfo->nHeaderBytes < 16 || |
103 | | // BigEndian classic TIFF |
104 | 134k | memcmp(poOpenInfo->pabyHeader, "\x4D\x4D\x00\x2A", 4) != 0) |
105 | 568k | { |
106 | 568k | return false; |
107 | 568k | } |
108 | | |
109 | 248 | struct MyFileReader final : public LIBERTIFF_NS::FileReader |
110 | 248 | { |
111 | 248 | const GDALOpenInfo *m_poOpenInfo; |
112 | | |
113 | 248 | explicit MyFileReader(const GDALOpenInfo *poOpenInfoIn) |
114 | 248 | : m_poOpenInfo(poOpenInfoIn) |
115 | 248 | { |
116 | 248 | } |
117 | | |
118 | 248 | uint64_t size() const override |
119 | 248 | { |
120 | 203 | return m_poOpenInfo->nHeaderBytes; |
121 | 203 | } |
122 | | |
123 | 248 | size_t read(uint64_t offset, size_t count, void *buffer) const override |
124 | 35.1k | { |
125 | 35.1k | if (offset + count > |
126 | 35.1k | static_cast<uint64_t>(m_poOpenInfo->nHeaderBytes)) |
127 | 185 | return 0; |
128 | 34.9k | memcpy(buffer, |
129 | 34.9k | m_poOpenInfo->pabyHeader + static_cast<size_t>(offset), |
130 | 34.9k | count); |
131 | 34.9k | return count; |
132 | 35.1k | } |
133 | | |
134 | 248 | CPL_DISALLOW_COPY_ASSIGN(MyFileReader) |
135 | 248 | }; |
136 | | |
137 | 248 | auto f = std::make_shared<const MyFileReader>(poOpenInfo); |
138 | | #ifdef DEBUG |
139 | | // Just to increase coverage testing |
140 | | CPLAssert(f->size() == uint64_t(poOpenInfo->nHeaderBytes)); |
141 | | char dummy = 0; |
142 | | CPLAssert(f->read(poOpenInfo->nHeaderBytes, 1, &dummy) == 0); |
143 | | CPL_IGNORE_RET_VAL(dummy); |
144 | | #endif |
145 | 248 | auto image = LIBERTIFF_NS::open</*acceptBigTIFF = */ false>(std::move(f)); |
146 | | // Checks that it is a single-band Float32 uncompressed dataset, made |
147 | | // of a single strip. |
148 | 248 | if (!image || image->nextImageOffset() != 0 || |
149 | 52 | image->compression() != LIBERTIFF_NS::Compression::None || |
150 | 41 | image->sampleFormat() != LIBERTIFF_NS::SampleFormat::IEEEFP || |
151 | 0 | image->samplesPerPixel() != 1 || image->bitsPerSample() != 32 || |
152 | 0 | image->isTiled() || image->strileCount() != 1 || image->width() == 0 || |
153 | 0 | image->width() > INT_MAX / sizeof(float) || image->height() == 0 || |
154 | 0 | image->height() > INT_MAX || image->rowsPerStrip() != image->height() || |
155 | 0 | !image->tag(LIBERTIFF_NS::TagCode::GeoTIFFPixelScale) || |
156 | 0 | !image->tag(LIBERTIFF_NS::TagCode::GeoTIFFTiePoints) || |
157 | 0 | !image->tag(LIBERTIFF_NS::TagCode::GeoTIFFGeoKeyDirectory) || |
158 | 0 | !image->tag(DIMAP_TAG)) |
159 | 248 | { |
160 | 248 | return false; |
161 | 248 | } |
162 | | |
163 | 0 | return true; |
164 | 248 | } |
165 | | |
166 | | /************************************************************************/ |
167 | | /* Open() */ |
168 | | /************************************************************************/ |
169 | | |
170 | | GDALDataset *SNAPTIFFDataset::Open(GDALOpenInfo *poOpenInfo) |
171 | 0 | { |
172 | 0 | if (poOpenInfo->eAccess == GA_Update || !Identify(poOpenInfo)) |
173 | 0 | return nullptr; |
174 | | |
175 | 0 | bool bIsGeolocation = false; |
176 | | // Check if it is SNAP_TIFF:"filename":{subdataset_component} syntax |
177 | 0 | if (STARTS_WITH(poOpenInfo->pszFilename, SNAP_TIFF_PREFIX)) |
178 | 0 | { |
179 | 0 | const CPLStringList aosTokens(CSLTokenizeString2( |
180 | 0 | poOpenInfo->pszFilename, ":", CSLT_HONOURSTRINGS)); |
181 | 0 | if (aosTokens.size() != 3) |
182 | 0 | return nullptr; |
183 | 0 | bIsGeolocation = EQUAL(aosTokens[2], GDAL_MDD_GEOLOCATION); |
184 | 0 | if (!bIsGeolocation && !EQUAL(aosTokens[2], "MAIN")) |
185 | 0 | return nullptr; |
186 | 0 | GDALOpenInfo oOpenInfo(aosTokens[1], GA_ReadOnly); |
187 | 0 | if (!Identify(&oOpenInfo)) |
188 | 0 | return nullptr; |
189 | 0 | std::swap(poOpenInfo->fpL, oOpenInfo.fpL); |
190 | 0 | if (!poOpenInfo->fpL) |
191 | 0 | return nullptr; |
192 | 0 | } |
193 | | |
194 | 0 | struct MyFileReader final : public LIBERTIFF_NS::FileReader |
195 | 0 | { |
196 | 0 | VSILFILE *m_fp; |
197 | 0 | mutable uint64_t m_nFileSize = 0; |
198 | |
|
199 | 0 | explicit MyFileReader(VSILFILE *fp) : m_fp(fp) |
200 | 0 | { |
201 | 0 | } |
202 | |
|
203 | 0 | uint64_t size() const override |
204 | 0 | { |
205 | 0 | if (m_nFileSize == 0) |
206 | 0 | { |
207 | 0 | m_fp->Seek(0, SEEK_END); |
208 | 0 | m_nFileSize = m_fp->Tell(); |
209 | 0 | } |
210 | 0 | return m_nFileSize; |
211 | 0 | } |
212 | |
|
213 | 0 | size_t read(uint64_t offset, size_t count, void *buffer) const override |
214 | 0 | { |
215 | 0 | return m_fp->Seek(offset, SEEK_SET) == 0 |
216 | 0 | ? m_fp->Read(buffer, 1, count) |
217 | 0 | : 0; |
218 | 0 | } |
219 | |
|
220 | 0 | CPL_DISALLOW_COPY_ASSIGN(MyFileReader) |
221 | 0 | }; |
222 | |
|
223 | 0 | auto f = std::make_shared<const MyFileReader>(poOpenInfo->fpL); |
224 | | #ifdef DEBUG |
225 | | // Just to increase coverage testing |
226 | | char dummy = 0; |
227 | | CPLAssert(f->read(f->size(), 1, &dummy) == 0); |
228 | | CPL_IGNORE_RET_VAL(dummy); |
229 | | #endif |
230 | |
|
231 | 0 | auto poDS = std::make_unique<SNAPTIFFDataset>(); |
232 | |
|
233 | 0 | poDS->m_poImage = LIBERTIFF_NS::open(std::move(f)); |
234 | 0 | if (!poDS->m_poImage) |
235 | 0 | return nullptr; |
236 | 0 | poDS->m_poFile.reset(poOpenInfo->fpL); |
237 | 0 | poOpenInfo->fpL = nullptr; |
238 | |
|
239 | 0 | poDS->nRasterXSize = static_cast<int>(poDS->m_poImage->width()); |
240 | 0 | poDS->nRasterYSize = static_cast<int>(poDS->m_poImage->height()); |
241 | 0 | poDS->SetDescription(poOpenInfo->pszFilename); |
242 | |
|
243 | 0 | if (bIsGeolocation) |
244 | 0 | { |
245 | 0 | poDS->m_bIsGeolocArray = true; |
246 | 0 | if (!poDS->GetGeolocationMetadata()) |
247 | 0 | { |
248 | 0 | return nullptr; |
249 | 0 | } |
250 | 0 | poDS->nRasterXSize = poDS->m_nGeolocArrayWidth; |
251 | 0 | poDS->nRasterYSize = poDS->m_nGeolocArrayHeight; |
252 | |
|
253 | 0 | const auto *psTag = |
254 | 0 | poDS->m_poImage->tag(LIBERTIFF_NS::TagCode::GeoTIFFTiePoints); |
255 | 0 | assert(psTag); |
256 | | |
257 | 0 | for (int iBand = 1; iBand <= 2; ++iBand) |
258 | 0 | { |
259 | 0 | auto poBand = std::make_unique<RawRasterBand>( |
260 | 0 | poDS->m_poFile.get(), |
261 | 0 | psTag->value_offset + |
262 | 0 | (iBand == 1 ? GCP_X : GCP_Y) * sizeof(double), |
263 | 0 | int(sizeof(double)) * VALUES_PER_GCP, |
264 | 0 | int(sizeof(double)) * VALUES_PER_GCP * poDS->nRasterXSize, |
265 | 0 | GDT_Float64, !poDS->m_poImage->mustByteSwap(), |
266 | 0 | poDS->nRasterXSize, poDS->nRasterYSize, |
267 | 0 | RawRasterBand::OwnFP::NO); |
268 | 0 | if (!poBand->IsValid()) |
269 | 0 | return nullptr; |
270 | 0 | if (iBand == 1) |
271 | 0 | poBand->SetDescription("Longitude"); |
272 | 0 | else |
273 | 0 | poBand->SetDescription("Latitude"); |
274 | 0 | poDS->SetBand(iBand, std::move(poBand)); |
275 | 0 | } |
276 | | |
277 | 0 | return poDS.release(); |
278 | 0 | } |
279 | | |
280 | 0 | poDS->ReadSRS(); |
281 | 0 | poDS->GetGeolocationMetadata(); |
282 | |
|
283 | 0 | { |
284 | 0 | bool okStrileOffset = false; |
285 | 0 | auto poBand = std::make_unique<RawRasterBand>( |
286 | 0 | poDS->m_poFile.get(), |
287 | 0 | poDS->m_poImage->strileOffset(0, okStrileOffset), |
288 | 0 | int(sizeof(float)), int(sizeof(float)) * poDS->nRasterXSize, |
289 | 0 | GDT_Float32, !poDS->m_poImage->mustByteSwap(), poDS->nRasterXSize, |
290 | 0 | poDS->nRasterYSize, RawRasterBand::OwnFP::NO); |
291 | 0 | if (!poBand->IsValid()) |
292 | 0 | return nullptr; |
293 | 0 | poDS->SetBand(1, std::move(poBand)); |
294 | 0 | } |
295 | 0 | auto poBand = poDS->papoBands[0]; |
296 | |
|
297 | 0 | const auto &psImageDescription = |
298 | 0 | poDS->m_poImage->tag(LIBERTIFF_NS::TagCode::ImageDescription); |
299 | 0 | if (psImageDescription && |
300 | 0 | psImageDescription->type == LIBERTIFF_NS::TagType::ASCII && |
301 | 0 | !psImageDescription->invalid_value_offset && |
302 | | // Sanity check |
303 | 0 | psImageDescription->count < 100 * 1000) |
304 | 0 | { |
305 | 0 | bool ok = true; |
306 | 0 | const std::string s = |
307 | 0 | poDS->m_poImage->readTagAsString(*psImageDescription, ok); |
308 | 0 | if (ok) |
309 | 0 | { |
310 | 0 | poDS->GDALDataset::SetMetadataItem("IMAGE_DESCRIPTION", s.c_str()); |
311 | 0 | } |
312 | 0 | } |
313 | |
|
314 | 0 | const auto *psDimapTag = poDS->m_poImage->tag(DIMAP_TAG); |
315 | 0 | if (psDimapTag && psDimapTag->type == LIBERTIFF_NS::TagType::ASCII && |
316 | 0 | !psDimapTag->invalid_value_offset) |
317 | 0 | { |
318 | 0 | try |
319 | 0 | { |
320 | 0 | bool ok = true; |
321 | | // Just read the first 10 kB max to fetch essential band metadata |
322 | 0 | const std::string s = poDS->m_poImage->readContext()->readString( |
323 | 0 | psDimapTag->value_offset, |
324 | 0 | static_cast<size_t>( |
325 | 0 | std::min<uint64_t>(psDimapTag->count, 10000)), |
326 | 0 | ok); |
327 | 0 | const auto posStart = s.find("<Spectral_Band_Info>"); |
328 | 0 | if (posStart != std::string::npos) |
329 | 0 | { |
330 | 0 | const char *markerEnd = "</Spectral_Band_Info>"; |
331 | 0 | const auto posEnd = s.find(markerEnd, posStart); |
332 | 0 | if (posEnd != std::string::npos) |
333 | 0 | { |
334 | 0 | const std::string osSubstr = s.substr( |
335 | 0 | posStart, posEnd - posStart + strlen(markerEnd)); |
336 | 0 | CPLXMLTreeCloser oRoot(CPLParseXMLString(osSubstr.c_str())); |
337 | 0 | if (oRoot) |
338 | 0 | { |
339 | 0 | const char *pszNoDataValueUsed = CPLGetXMLValue( |
340 | 0 | oRoot.get(), "NO_DATA_VALUE_USED", nullptr); |
341 | 0 | const char *pszNoDataValue = CPLGetXMLValue( |
342 | 0 | oRoot.get(), "NO_DATA_VALUE", nullptr); |
343 | 0 | if (pszNoDataValueUsed && pszNoDataValue && |
344 | 0 | CPLTestBool(pszNoDataValueUsed)) |
345 | 0 | { |
346 | 0 | poBand->SetNoDataValue(CPLAtof(pszNoDataValue)); |
347 | 0 | } |
348 | |
|
349 | 0 | if (const char *pszScalingFactor = CPLGetXMLValue( |
350 | 0 | oRoot.get(), "SCALING_FACTOR", nullptr)) |
351 | 0 | { |
352 | 0 | poBand->SetScale(CPLAtof(pszScalingFactor)); |
353 | 0 | } |
354 | |
|
355 | 0 | if (const char *pszScalingOffset = CPLGetXMLValue( |
356 | 0 | oRoot.get(), "SCALING_OFFSET", nullptr)) |
357 | 0 | { |
358 | 0 | poBand->SetOffset(CPLAtof(pszScalingOffset)); |
359 | 0 | } |
360 | |
|
361 | 0 | if (const char *pszBandName = CPLGetXMLValue( |
362 | 0 | oRoot.get(), "BAND_NAME", nullptr)) |
363 | 0 | { |
364 | 0 | poBand->SetDescription(pszBandName); |
365 | 0 | } |
366 | |
|
367 | 0 | if (const char *pszUnit = CPLGetXMLValue( |
368 | 0 | oRoot.get(), "PHYSICAL_UNIT", nullptr)) |
369 | 0 | { |
370 | 0 | poBand->SetUnitType(pszUnit); |
371 | 0 | } |
372 | 0 | } |
373 | 0 | } |
374 | 0 | } |
375 | 0 | } |
376 | 0 | catch (const std::exception &) |
377 | 0 | { |
378 | 0 | } |
379 | 0 | } |
380 | | |
381 | | // Initialize PAM |
382 | 0 | poDS->SetDescription(poOpenInfo->pszFilename); |
383 | 0 | poDS->TryLoadXML(); |
384 | | |
385 | | // Check for overviews. |
386 | 0 | poDS->oOvManager.Initialize(poDS.get(), poOpenInfo->pszFilename); |
387 | |
|
388 | 0 | return poDS.release(); |
389 | 0 | } |
390 | | |
391 | | /************************************************************************/ |
392 | | /* GetMetadataDomainList() */ |
393 | | /************************************************************************/ |
394 | | |
395 | | char **SNAPTIFFDataset::GetMetadataDomainList() |
396 | 0 | { |
397 | 0 | return BuildMetadataDomainList(GDALPamDataset::GetMetadataDomainList(), |
398 | 0 | TRUE, GDAL_MDD_GEOLOCATION, |
399 | 0 | GDAL_MDD_SUBDATASETS, "xml:DIMAP", nullptr); |
400 | 0 | } |
401 | | |
402 | | /************************************************************************/ |
403 | | /* GetGeolocationMetadata() */ |
404 | | /************************************************************************/ |
405 | | |
406 | | // (Partially) read the content of the GeoTIFFTiePoints tag to check if the |
407 | | // tie points form a regular geolocation array, and extract the width, height, |
408 | | // and spacing of that geolocation array. Also fills the m_aosGEOLOCATION |
409 | | // metadata domain |
410 | | bool SNAPTIFFDataset::GetGeolocationMetadata() |
411 | 0 | { |
412 | 0 | const auto *psTag = m_poImage->tag(LIBERTIFF_NS::TagCode::GeoTIFFTiePoints); |
413 | 0 | if (psTag && psTag->type == LIBERTIFF_NS::TagType::Double && |
414 | 0 | !psTag->invalid_value_offset && (psTag->count % VALUES_PER_GCP) == 0 && |
415 | | // Sanity check |
416 | 0 | psTag->count <= uint64_t(nRasterXSize) * nRasterYSize * VALUES_PER_GCP) |
417 | 0 | { |
418 | 0 | const int numGCPs = int(psTag->count / VALUES_PER_GCP); |
419 | | |
420 | | // Assume that the geolocation array has the same proportion as the |
421 | | // main array |
422 | 0 | const double dfGCPArrayWidth = |
423 | 0 | sqrt(double(nRasterXSize) * numGCPs / nRasterYSize); |
424 | 0 | const double dfGCPArrayHeight = |
425 | 0 | sqrt(double(nRasterYSize) * numGCPs / nRasterXSize); |
426 | 0 | if (dfGCPArrayWidth > INT_MAX || dfGCPArrayHeight > INT_MAX) |
427 | 0 | { |
428 | 0 | return false; |
429 | 0 | } |
430 | | |
431 | 0 | const int nGCPArrayWidth = int(std::round(dfGCPArrayWidth)); |
432 | 0 | const int nGCPArrayHeight = int(std::round(dfGCPArrayHeight)); |
433 | 0 | constexpr int NUM_LINES = 3; |
434 | 0 | if (nGCPArrayWidth * nGCPArrayHeight != numGCPs || |
435 | 0 | nGCPArrayHeight < NUM_LINES) |
436 | 0 | { |
437 | 0 | return false; |
438 | 0 | } |
439 | | |
440 | 0 | bool ok = true; |
441 | | // Just read the first 3 lines of the geolocation array |
442 | 0 | const auto numValuesPerLine = nGCPArrayWidth * VALUES_PER_GCP; |
443 | 0 | const auto values = m_poImage->readContext()->readArray<double>( |
444 | 0 | psTag->value_offset, numValuesPerLine * NUM_LINES, ok); |
445 | 0 | if (!ok || values.empty()) |
446 | 0 | return false; |
447 | | |
448 | 0 | if (values[GCP_LINE] != 0.5 && values[GCP_PIXEL] != 0.5) |
449 | 0 | return false; |
450 | | |
451 | 0 | constexpr double RELATIVE_TOLERANCE = 1e-5; |
452 | 0 | constexpr double PIXEL_TOLERANCE = 1e-3; |
453 | | |
454 | | // Check that pixel spacing is constant on all three first lines |
455 | 0 | const double pixelSpacing = |
456 | 0 | values[VALUES_PER_GCP + GCP_PIXEL] - values[GCP_PIXEL]; |
457 | 0 | if (!(pixelSpacing >= 1)) |
458 | 0 | return false; |
459 | 0 | if (std::fabs(pixelSpacing * (nGCPArrayWidth - 1) - |
460 | 0 | (nRasterXSize - 1)) > PIXEL_TOLERANCE) |
461 | 0 | return false; |
462 | | |
463 | 0 | double adfY[NUM_LINES]; |
464 | 0 | for (int iLine = 0; iLine < NUM_LINES; ++iLine) |
465 | 0 | { |
466 | 0 | adfY[iLine] = values[iLine * numValuesPerLine + GCP_LINE]; |
467 | 0 | for (int i = iLine * numValuesPerLine + VALUES_PER_GCP; |
468 | 0 | i < (iLine + 1) * numValuesPerLine; i += VALUES_PER_GCP) |
469 | 0 | { |
470 | 0 | if (values[i + GCP_LINE] != |
471 | 0 | values[i - VALUES_PER_GCP + GCP_LINE]) |
472 | 0 | { |
473 | 0 | return false; |
474 | 0 | } |
475 | 0 | const double pixelSpacingNew = |
476 | 0 | values[i + GCP_PIXEL] - |
477 | 0 | values[i - VALUES_PER_GCP + GCP_PIXEL]; |
478 | 0 | if (std::fabs(pixelSpacingNew - pixelSpacing) > |
479 | 0 | RELATIVE_TOLERANCE * std::fabs(pixelSpacing)) |
480 | 0 | { |
481 | 0 | return false; |
482 | 0 | } |
483 | 0 | } |
484 | 0 | } |
485 | | |
486 | | // Check that line spacing is constant on the three first lines |
487 | 0 | const double lineSpacing = adfY[1] - adfY[0]; |
488 | 0 | if (!(lineSpacing >= 1)) |
489 | 0 | return false; |
490 | 0 | if (std::fabs(lineSpacing * (nGCPArrayHeight - 1) - |
491 | 0 | (nRasterYSize - 1)) > PIXEL_TOLERANCE) |
492 | 0 | return false; |
493 | | |
494 | 0 | for (int iLine = 1; iLine + 1 < NUM_LINES; ++iLine) |
495 | 0 | { |
496 | 0 | const double lineSpacingNew = adfY[iLine + 1] - adfY[iLine]; |
497 | 0 | if (std::fabs(lineSpacingNew - lineSpacing) > |
498 | 0 | RELATIVE_TOLERANCE * std::fabs(lineSpacing)) |
499 | 0 | { |
500 | 0 | return false; |
501 | 0 | } |
502 | 0 | } |
503 | | |
504 | | // Read last line |
505 | 0 | const auto lastLineValue = m_poImage->readContext()->readArray<double>( |
506 | 0 | psTag->value_offset + uint64_t(nGCPArrayHeight - 1) * |
507 | 0 | numValuesPerLine * sizeof(double), |
508 | 0 | numValuesPerLine, ok); |
509 | 0 | if (!ok) |
510 | 0 | return false; |
511 | | |
512 | 0 | if (!m_bIsGeolocArray) |
513 | 0 | { |
514 | | // Expose the 4 corner GCPs for rough georeferencing. |
515 | |
|
516 | 0 | const uint32_t nShift = numValuesPerLine - VALUES_PER_GCP; |
517 | 0 | m_asGCPs.emplace_back("TL", "Top Left", values[GCP_PIXEL], |
518 | 0 | values[GCP_LINE], values[GCP_X], |
519 | 0 | values[GCP_Y], values[GCP_Z]); |
520 | 0 | m_asGCPs.emplace_back( |
521 | 0 | "TR", "Top Right", values[nShift + GCP_PIXEL], |
522 | 0 | values[nShift + GCP_LINE], values[nShift + GCP_X], |
523 | 0 | values[nShift + GCP_Y], values[nShift + GCP_Z]); |
524 | 0 | m_asGCPs.emplace_back("BL", "Bottom Left", lastLineValue[GCP_PIXEL], |
525 | 0 | lastLineValue[GCP_LINE], lastLineValue[GCP_X], |
526 | 0 | lastLineValue[GCP_Y], lastLineValue[GCP_Z]); |
527 | 0 | m_asGCPs.emplace_back( |
528 | 0 | "BR", "Bottom Right", lastLineValue[nShift + GCP_PIXEL], |
529 | 0 | lastLineValue[nShift + GCP_LINE], lastLineValue[nShift + GCP_X], |
530 | 0 | lastLineValue[nShift + GCP_Y], lastLineValue[nShift + GCP_Z]); |
531 | 0 | } |
532 | |
|
533 | 0 | m_nGeolocArrayWidth = nGCPArrayWidth; |
534 | 0 | m_nGeolocArrayHeight = nGCPArrayHeight; |
535 | |
|
536 | 0 | if (!m_bIsGeolocArray) |
537 | 0 | { |
538 | 0 | if (!m_oSRS.IsEmpty()) |
539 | 0 | { |
540 | 0 | char *pszWKT = nullptr; |
541 | 0 | m_oSRS.exportToWkt(&pszWKT); |
542 | 0 | m_aosGEOLOCATION.SetNameValue("SRS", pszWKT); |
543 | 0 | CPLFree(pszWKT); |
544 | 0 | } |
545 | |
|
546 | 0 | m_aosGEOLOCATION.SetNameValue( |
547 | 0 | "X_DATASET", CPLSPrintf("%s\"%s\":GEOLOCATION", |
548 | 0 | SNAP_TIFF_PREFIX, GetDescription())); |
549 | 0 | m_aosGEOLOCATION.SetNameValue("X_BAND", "1"); |
550 | 0 | m_aosGEOLOCATION.SetNameValue( |
551 | 0 | "Y_DATASET", CPLSPrintf("%s\"%s\":GEOLOCATION", |
552 | 0 | SNAP_TIFF_PREFIX, GetDescription())); |
553 | 0 | m_aosGEOLOCATION.SetNameValue("Y_BAND", "2"); |
554 | 0 | m_aosGEOLOCATION.SetNameValue("PIXEL_OFFSET", "0"); |
555 | 0 | m_aosGEOLOCATION.SetNameValue("PIXEL_STEP", |
556 | 0 | CPLSPrintf("%.17g", pixelSpacing)); |
557 | 0 | m_aosGEOLOCATION.SetNameValue("LINE_OFFSET", "0"); |
558 | 0 | m_aosGEOLOCATION.SetNameValue("LINE_STEP", |
559 | 0 | CPLSPrintf("%.17g", lineSpacing)); |
560 | 0 | } |
561 | |
|
562 | 0 | return true; |
563 | 0 | } |
564 | 0 | return false; |
565 | 0 | } |
566 | | |
567 | | /************************************************************************/ |
568 | | /* ReadSRS() */ |
569 | | /************************************************************************/ |
570 | | |
571 | | // Simplified GeoTIFF SRS reader, assuming the SRS is encoded as a EPSG code |
572 | | void SNAPTIFFDataset::ReadSRS() |
573 | 0 | { |
574 | 0 | const auto &psGeoKeysTag = |
575 | 0 | m_poImage->tag(LIBERTIFF_NS::TagCode::GeoTIFFGeoKeyDirectory); |
576 | 0 | constexpr int VALUES_PER_GEOKEY = 4; |
577 | 0 | if (psGeoKeysTag && psGeoKeysTag->type == LIBERTIFF_NS::TagType::Short && |
578 | 0 | !psGeoKeysTag->invalid_value_offset && |
579 | 0 | psGeoKeysTag->count >= VALUES_PER_GEOKEY && |
580 | 0 | (psGeoKeysTag->count % VALUES_PER_GEOKEY) == 0 && |
581 | | // Sanity check |
582 | 0 | psGeoKeysTag->count < 1000) |
583 | 0 | { |
584 | 0 | bool ok = true; |
585 | 0 | const auto values = |
586 | 0 | m_poImage->readTagAsVector<uint16_t>(*psGeoKeysTag, ok); |
587 | 0 | if (values.size() >= 4) |
588 | 0 | { |
589 | 0 | const uint16_t geokeysCount = values[3]; |
590 | 0 | constexpr uint16_t GEOTIFF_KEY_DIRECTORY_VERSION_V1 = 1; |
591 | 0 | constexpr uint16_t GEOTIFF_KEY_VERSION_MAJOR_V1 = 1; |
592 | 0 | if (values[0] == GEOTIFF_KEY_DIRECTORY_VERSION_V1 && |
593 | | // GeoTIFF 1.x |
594 | 0 | values[1] == GEOTIFF_KEY_VERSION_MAJOR_V1 && |
595 | 0 | geokeysCount == psGeoKeysTag->count / VALUES_PER_GEOKEY - 1) |
596 | 0 | { |
597 | 0 | constexpr uint16_t GeoTIFFTypeShort = 0; |
598 | 0 | constexpr uint16_t GeodeticCRSGeoKey = 2048; |
599 | 0 | constexpr uint16_t ProjectedCRSGeoKey = 3072; |
600 | 0 | uint16_t nEPSGCode = 0; |
601 | 0 | for (uint32_t i = 1; i <= geokeysCount; ++i) |
602 | 0 | { |
603 | 0 | const auto geokey = values[VALUES_PER_GEOKEY * i]; |
604 | 0 | const auto geokeyType = values[VALUES_PER_GEOKEY * i + 1]; |
605 | 0 | const auto geokeyCount = values[VALUES_PER_GEOKEY * i + 2]; |
606 | 0 | const auto geokeyValue = values[VALUES_PER_GEOKEY * i + 3]; |
607 | 0 | if ((geokey == GeodeticCRSGeoKey || |
608 | 0 | geokey == ProjectedCRSGeoKey) && |
609 | 0 | geokeyType == GeoTIFFTypeShort && geokeyCount == 1 && |
610 | 0 | geokeyValue > 0) |
611 | 0 | { |
612 | 0 | nEPSGCode = geokeyValue; |
613 | 0 | if (geokey == ProjectedCRSGeoKey) |
614 | 0 | break; |
615 | 0 | } |
616 | 0 | } |
617 | 0 | if (nEPSGCode > 0) |
618 | 0 | { |
619 | 0 | m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); |
620 | 0 | m_oSRS.importFromEPSG(nEPSGCode); |
621 | 0 | } |
622 | 0 | } |
623 | 0 | } |
624 | 0 | } |
625 | 0 | } |
626 | | |
627 | | /************************************************************************/ |
628 | | /* GetMetadata() */ |
629 | | /************************************************************************/ |
630 | | |
631 | | CSLConstList SNAPTIFFDataset::GetMetadata(const char *pszDomain) |
632 | 0 | { |
633 | 0 | if (!m_bIsGeolocArray) |
634 | 0 | { |
635 | 0 | if (pszDomain && EQUAL(pszDomain, "xml:DIMAP")) |
636 | 0 | { |
637 | 0 | if (m_aosDIMAPMetadata.empty()) |
638 | 0 | { |
639 | 0 | const auto *psDimapTag = m_poImage->tag(DIMAP_TAG); |
640 | 0 | if (psDimapTag && |
641 | 0 | psDimapTag->type == LIBERTIFF_NS::TagType::ASCII && |
642 | 0 | !psDimapTag->invalid_value_offset && |
643 | | // Sanity check |
644 | 0 | psDimapTag->count < 100 * 1000 * 1000) |
645 | 0 | { |
646 | 0 | try |
647 | 0 | { |
648 | 0 | bool ok = true; |
649 | 0 | const std::string s = |
650 | 0 | m_poImage->readTagAsString(*psDimapTag, ok); |
651 | 0 | if (ok) |
652 | 0 | { |
653 | 0 | m_aosDIMAPMetadata.AddString(s.c_str()); |
654 | 0 | } |
655 | 0 | } |
656 | 0 | catch (const std::exception &) |
657 | 0 | { |
658 | 0 | CPLError(CE_Failure, CPLE_OutOfMemory, |
659 | 0 | "Out of memory in GetMetadata()"); |
660 | 0 | } |
661 | 0 | } |
662 | 0 | } |
663 | 0 | return m_aosDIMAPMetadata.List(); |
664 | 0 | } |
665 | 0 | else if (pszDomain && EQUAL(pszDomain, GDAL_MDD_GEOLOCATION)) |
666 | 0 | { |
667 | 0 | return m_aosGEOLOCATION.List(); |
668 | 0 | } |
669 | 0 | else if (pszDomain && EQUAL(pszDomain, GDAL_MDD_SUBDATASETS)) |
670 | 0 | { |
671 | 0 | if (m_aosSUBDATASETS.empty() && GetGeolocationMetadata()) |
672 | 0 | { |
673 | 0 | m_aosSUBDATASETS.SetNameValue("SUBDATASET_1_NAME", |
674 | 0 | CPLSPrintf("%s\"%s\":MAIN", |
675 | 0 | SNAP_TIFF_PREFIX, |
676 | 0 | GetDescription())); |
677 | 0 | m_aosSUBDATASETS.SetNameValue("SUBDATASET_1_DESC", |
678 | 0 | std::string("Main content of ") |
679 | 0 | .append(GetDescription()) |
680 | 0 | .c_str()); |
681 | |
|
682 | 0 | m_aosSUBDATASETS.SetNameValue( |
683 | 0 | "SUBDATASET_2_NAME", |
684 | 0 | m_aosGEOLOCATION.FetchNameValue("X_DATASET")); |
685 | 0 | m_aosSUBDATASETS.SetNameValue( |
686 | 0 | "SUBDATASET_2_DESC", std::string("Geolocation array of ") |
687 | 0 | .append(GetDescription()) |
688 | 0 | .c_str()); |
689 | 0 | } |
690 | 0 | return m_aosSUBDATASETS.List(); |
691 | 0 | } |
692 | 0 | } |
693 | | |
694 | 0 | return GDALPamDataset::GetMetadata(pszDomain); |
695 | 0 | } |
696 | | |
697 | | /************************************************************************/ |
698 | | /* GetMetadataItem() */ |
699 | | /************************************************************************/ |
700 | | |
701 | | const char *SNAPTIFFDataset::GetMetadataItem(const char *pszName, |
702 | | const char *pszDomain) |
703 | 0 | { |
704 | 0 | if (!m_bIsGeolocArray && pszDomain && |
705 | 0 | (EQUAL(pszDomain, GDAL_MDD_GEOLOCATION) || |
706 | 0 | EQUAL(pszDomain, GDAL_MDD_SUBDATASETS))) |
707 | 0 | { |
708 | 0 | return CSLFetchNameValue(GetMetadata(pszDomain), pszName); |
709 | 0 | } |
710 | 0 | return GDALPamDataset::GetMetadataItem(pszName, pszDomain); |
711 | 0 | } |
712 | | |
713 | | /************************************************************************/ |
714 | | /* GDALRegister_SNAP_TIFF() */ |
715 | | /************************************************************************/ |
716 | | |
717 | | void GDALRegister_SNAP_TIFF() |
718 | 24 | { |
719 | 24 | if (GDALGetDriverByName("SNAP_TIFF") != nullptr) |
720 | 0 | return; |
721 | | |
722 | 24 | GDALDriver *poDriver = new GDALDriver(); |
723 | 24 | poDriver->SetDescription("SNAP_TIFF"); |
724 | 24 | poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES"); |
725 | 24 | poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, |
726 | 24 | "Sentinel Application Processing GeoTIFF"); |
727 | 24 | poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, |
728 | 24 | "drivers/raster/snap_tiff.html"); |
729 | | // Declaring the tif extension confuses QGIS |
730 | | // Cf https://github.com/qgis/QGIS/issues/59112 |
731 | | // This driver is of too marginal usage to justify causing chaos downstream. |
732 | | // poDriver->SetMetadataItem(GDAL_DMD_MIMETYPE, "image/tiff"); |
733 | | // poDriver->SetMetadataItem(GDAL_DMD_EXTENSIONS, "tif tiff"); |
734 | 24 | poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES"); |
735 | | |
736 | 24 | poDriver->pfnOpen = SNAPTIFFDataset::Open; |
737 | 24 | poDriver->pfnIdentify = SNAPTIFFDataset::Identify; |
738 | | |
739 | 24 | GetGDALDriverManager()->RegisterDriver(poDriver); |
740 | 24 | } |