13 KiB
GeoTIFF Import Strategy Research Note
Status: Decision implemented. The recommended pure-Rust path has landed as the optional
import-geotifffeature; the crate survey below is kept for provenance.Context: Phase 4 Milestone C, Task C3 ("GeoTIFF importer behind an optional feature"). This note recorded the crate survey and recommended direction; the "Implementation notes" section below describes what was actually built.
Recommendation
Start with a pure-Rust, opt-in GeoTIFF read experiment behind an
import-geotiff feature, using geotiff-reader (with its geotiff-core
support crate) as the first parser. Do not pull GDAL into the default build,
the test suite, or CI. Treat any GDAL-backed path as future, separately
reviewed work.
Rationale:
- No native dependency in the default build. Phase 4 explicitly requires
default builds to stay clean and fast (
cargo fmt --check,cargo test,cargo clippy,git diff --check). A pure-Rust crate keeps GeoTIFF support fully optional and removes the C toolchain /libgdalinstall burden from contributors and CI. - License compatibility.
geotiff-readerisMIT OR Apache-2.0, matching the project's clean-room, permissively licensed posture. GDAL itself is MIT-licensed at the library level, but a vendored/native build drags in a large transitive dependency surface that is harder to audit. - Scope fit. The importer only needs to produce a
HeightGridplusTerrainSourceMetadatafrom a single-band elevation raster. That is a narrow read-only subset; a full GDAL binding is overkill for the spike. - Reversible. Keeping GeoTIFF behind a feature flag means the crate choice can be swapped (or upgraded to GDAL) later without disturbing default users.
The first slice should parse a tiny generated synthetic GeoTIFF fixture only — no proprietary VistaPro data, no large downloaded DEMs.
Decision matrix
Facts below are from cargo info at spike time. "GeoTIFF semantics" means the
crate understands GeoTIFF's geo-tags (model tie point / pixel scale, CRS,
elevation band), not just baseline TIFF tags.
Pure-Rust candidates
| Crate | Version | License | MSRV | Native deps | GeoTIFF semantics | Notes |
|---|---|---|---|---|---|---|
geotiff-reader (+ geotiff-core) |
0.4.0 | MIT OR Apache-2.0 | rust 1.77 | None | Yes (read-only) | Recommended. Pure Rust, read-only; optional cog feature pulls reqwest for Cloud-Optimized GeoTIFF over HTTP — leave cog off. |
tiff-reader |
0.4.0 | MIT OR Apache-2.0 | — | None ("no C deps") | Partial / TIFF-level | Pure-Rust TIFF reader, no C deps; useful fallback or low-level reader, but not a full GeoTIFF semantics layer on its own. |
geotiff (georust, older) |
0.1.0 | MIT | rust 1.70 | None | Yes (limited) | Older, low-version georust crate; narrow API and stale. Acceptable fallback only if geotiff-reader regresses. |
image TIFF (image-rs/tiff) |
0.10.3 | MIT | rust 1.74 | None | No | Robust pure-Rust TIFF decode/encode, already adjacent to the project's image use, but it does not interpret GeoTIFF geo-tags. Could back a hand-rolled geo-tag reader if needed. |
wbraster |
0.1.5 | MIT OR Apache-2.0 | — | None (Rust) | Raster I/O | Whitebox raster abstraction; broad raster model, early-stage (0.1.x), heavier than the spike needs. |
wbgeotiff |
0.1.2 | MIT OR Apache-2.0 | — | None (Rust) | Yes | Whitebox GeoTIFF reader/writer; very early (0.1.2). Watch as an alternative if geotiff-reader stalls. |
georaster |
0.2.0 | MIT/Apache-2.0 | — | None | Yes (raster) | Pure-Rust georeferenced raster reader; reasonable secondary option, still 0.2.x. |
GDAL-backed candidates
| Crate | Version | License | MSRV | Native deps | Notes |
|---|---|---|---|---|---|
gdal |
0.19.0 | MIT | rust 1.80 | Yes — system libgdal, or gdal-src/bindgen to build from source |
Mature, full-format coverage. Native GDAL is the main risk: install friction, version skew, large audit surface, brittle CI. Optional gdal-src vendoring trades that for long C/C++ build times and a bindgen/Clang requirement. |
gdal-sys |
0.12.0 | MIT | rust 1.77 | Yes — raw FFI bindings to libgdal |
Low-level; never used directly here. Same native risk as gdal. |
rstiff |
0.2.0 | MIT | — | Yes — "powered by GDAL" | A GeoTIFF-focused wrapper, but still GDAL-backed, so it carries the same native dependency risk; early-stage (0.2.x). |
oxigdal-geotiff |
0.1.4 | Apache-2.0 | rust 1.85 | None declared; pure-Rust OxiGDAL driver | Newest entrant, high MSRV (rust 1.85), very early (0.1.4). Too immature and too new-MSRV for the spike even though it avoids native GDAL. |
Licensing assessment
- Project posture: clean-room, open-source, permissively licensed (see
docs/legal/asset-policy.md). geotiff-reader/geotiff-core,tiff-reader,wbraster,wbgeotiff, andgeorasterare allMIT OR Apache-2.0(orMIT/Apache-2.0) — fully compatible.image's TIFF crate,geotiff(georust),gdal,gdal-sys, andrstiffare MIT — compatible.oxigdal-geotiffis Apache-2.0 only — compatible, but it removes the dual MIT/Apache option for downstream consumers of that path.- No copyleft crate appears in the survey. The recommended
geotiff-readerpath keeps the dualMIT OR Apache-2.0license clean throughout. - Fixtures, not crates, are the real licensing hazard. Per
asset-policy.md, do not commit proprietary sample landscapes or data. GeoTIFF test inputs must be synthetic and generated by this project, or public-domain (USGS/NASA) and redistributed under documented source terms.
Native dependency risk
- A GDAL-backed crate (
gdal,gdal-sys,rstiff) requires either a systemlibgdal(version drift across developer machines, distros, and CI) or a source build viagdal-src(long C/C++ compile,bindgen+ Clang toolchain). Either path makes default builds slow or fragile and enlarges the audited dependency surface. - Phase 4's plan (Task C3) explicitly anticipates this: "If GDAL is required,
keep the feature opt-in and document native setup," and the cross-milestone
checks expect
cargo test --features import-geotiffto either pass or produce "a documented skip if native GDAL is not installed." - Mitigation: the recommended pure-Rust path has zero native
dependencies, so
import-geotiffbuilds and tests run anywherecargoruns. The "documented skip" escape hatch is then unnecessary for the chosen path and is reserved only for a hypothetical future GDAL feature.
Feature flag plan
Consistent with Task C1's existing feature scaffold (import-dem,
import-hgt, import-geotiff under default = []):
import-geotiff— the spike target. Enables the pure-Rustgeotiff-reader+geotiff-coredependency and thesrc/import/geotiff.rsmodule. Off by default.- Do not enable
geotiff-reader's optionalcogfeature; it pullsreqwestand network/TLS dependencies that are out of scope for local file import. - Reserve a separate, not-yet-created feature name (e.g.
import-geotiff-gdal) for any future GDAL-backed path so the two backends never share a flag. This keeps the native-dependency build strictly opt-in and clearly labeled. - Default and
--no-default-featuresbuilds must remain GeoTIFF-free; onlycargo test --features import-geotiffand--all-featuresexercise it.
Fixture and test strategy
- Synthetic only. Generate a tiny single-band elevation GeoTIFF (e.g. 3×3 or 4×4, signed/float elevation samples, a simple model tie point + pixel scale, a basic CRS tag) as a project-owned fixture. This mirrors the HGT approach in Task B1, which uses inline synthetic bytes.
- Generation, not download. Prefer generating the fixture programmatically
(a small build helper, a checked-in generator test, or a documented
one-off). If a writer crate is needed,
image's TIFF encoder orwbgeotiffcan produce baseline bytes; keep any generator code clearly separate from the parser slice per the plan's commit strategy. - What to assert: parsed dimensions, elevation unit, a couple of known
sample values, and that geo metadata maps into
TerrainSourceMetadata(format"geotiff", width, height,ElevationUnit). - Malformed-input tests: truncated file, missing geo-tags, unsupported
multi-band or compressed input → typed
ImportErrorvariants, matching the rejection style of Task B2. - No proprietary data, no large DEMs in the repo. A real USGS/NASA tile may
be used locally for manual verification but must stay out of git
(
reference/,.work/are already ignored). - The committed fixture should be only as large as the test needs (target: well under a kilobyte).
Implementation notes
The recommended pure-Rust path has landed. What was built:
- Feature + dependency.
Cargo.tomldeclaresimport-geotiff = ["dep:geotiff-reader"];geotiff-readeris an optional dependency built with itslocalfeature anddefault-features = false, so the network-orientedcogfeature stays off. Default and--no-default-featuresbuilds remain GeoTIFF-free. src/import/geotiff.rs. Acfg(feature = "import-geotiff")module exposingparse_geotiff_bytes(&[u8]) -> Result<ImportedTerrain, ImportError>, reusing theImportedTerrain/ImportError/TerrainSourceMetadatatypes from Milestone A. It reads the payload in memory, rejects rasters that are not single-band, decodes the raster asf32into a row-majorHeightGrid, and recordsTerrainSourceMetadatawith format"geotiff". Reader and decode failures map toImportError::MalformedSource.- Synthetic fixtures only. No GeoTIFF binary is committed. Tests generate a
tiny single-band elevation tile in memory with the
geotiff-writerdev-dependency (alongsidendarray) and parse it back — mirroring the inline-bytes approach used for HGT. Real USGS/NASA tiles may be used for local manual verification only, under the already-ignoredreference/and.work/directories. - Tests.
cargo test geotiff --features import-geotiffasserts dimensions, sample values, and metadata, and checks that non-GeoTIFF input yields a typedImportError::MalformedSource. - Supported subset. Only the narrow single-band
f32case is parsed; broader real-world GeoTIFF coverage (multi-band, exotic compression, full geo-tag interpretation) remains future work. openvistapro info.supported_importers()andinfo_text()listgeotiffonly when the feature is built, keepinginfohonest.- GDAL is not part of the landed feature. The importer has zero native
dependencies; a GDAL-backed path stays out of scope until a separate review
approves the native-dependency cost, and would use its own opt-in feature
name rather than
import-geotiff.
Not yet done: CLI render plumbing. A render --import-geotiff <path> input
mirroring the HGT pattern (Task B3) is still future work; the parser is reachable
through the library API and reported by info, but not yet wired into render.
If geotiff-reader later proves insufficient (missing geo-tag coverage, API
gaps), the documented fallback order remains georaster → wbgeotiff →
hand-rolled geo-tag reading on top of image's TIFF decoder.
Sources
Survey performed via cargo info; verify current versions before
implementation.
geotiff-reader— https://crates.io/crates/geotiff-reader · https://docs.rs/geotiff-readergeotiff-core— https://crates.io/crates/geotiff-core · https://docs.rs/geotiff-coretiff-reader— https://crates.io/crates/tiff-reader · https://docs.rs/tiff-readergeotiff(georust) — https://crates.io/crates/geotiff · https://docs.rs/geotiff · https://github.com/georust/geotiffimageTIFF crate — https://crates.io/crates/tiff · https://docs.rs/tiff · https://github.com/image-rs/image-tiffgeoraster— https://crates.io/crates/georaster · https://docs.rs/georasterwbraster— https://crates.io/crates/wbraster · https://docs.rs/wbrasterwbgeotiff— https://crates.io/crates/wbgeotiff · https://docs.rs/wbgeotiffoxigdal-geotiff— https://crates.io/crates/oxigdal-geotiff · https://docs.rs/oxigdal-geotiffgdal— https://crates.io/crates/gdal · https://docs.rs/gdal · https://github.com/georust/gdalgdal-sys— https://crates.io/crates/gdal-sys · https://docs.rs/gdal-sysrstiff— https://crates.io/crates/rstiff · https://docs.rs/rstiff- Project constraints —
docs/legal/asset-policy.md,docs/plans/phase-4-formats-scripts-ui.md