GeoConvert 0.2.0
See the version list below for details.
dotnet add package GeoConvert --version 0.2.0
NuGet\Install-Package GeoConvert -Version 0.2.0
<PackageReference Include="GeoConvert" Version="0.2.0" />
<PackageVersion Include="GeoConvert" Version="0.2.0" />
<PackageReference Include="GeoConvert" />
paket add GeoConvert --version 0.2.0
#r "nuget: GeoConvert, 0.2.0"
#:package GeoConvert@0.2.0
#addin nuget:?package=GeoConvert&version=0.2.0
#tool nuget:?package=GeoConvert&version=0.2.0
<img src="/src/icon.png" height="30px"> GeoConvert
Convert maps between geospatial formats, with no third-party dependencies — only the .NET base
class libraries (System.Text.Json, System.Xml, System.IO.Compression). It can also render a
bounding box to a PNG image. Ships as a library and a geoconvert command line tool.
Supported formats
All vector formats can be both read and written; PNG is a write-only raster export.
| Format | Extension(s) | Kind |
|---|---|---|
| GeoJSON | .geojson, .json |
JSON |
| TopoJSON | .topojson |
JSON (topology encoded) |
| Shapefile | .shp (+ .shx, .dbf, .prj) |
Binary |
| FlatGeobuf | .fgb |
Binary (FlatBuffers) |
| KML | .kml |
XML |
| KMZ | .kmz |
Zipped KML |
| GPX | .gpx |
XML |
| WKT | .wkt |
Text |
| WKB | .wkb |
Binary |
| CSV | .csv |
Text (WKT or lon/lat columns) |
| GeoParquet | .parquet, .geoparquet |
Binary (Apache Parquet) |
| PNG | .png |
Raster image (write-only) |
All coordinates are treated as WGS84 (EPSG:4326) longitude/latitude.
Library
Convert a file to another format (both formats inferred from their extensions):
<a id='snippet-Convert'></a>
// Formats are inferred from the file extensions.
GeoConverter.Convert("cities.geojson", "cities.kml");
GeoConverter.Convert("roads.shp", "roads.fgb");
<sup><a href='/src/Tests/Snippets.cs#L6-L10' title='Snippet source file'>snippet source</a> | <a href='#snippet-Convert' title='Start of snippet'>anchor</a></sup>
Read into the common feature model, then write a different format:
<a id='snippet-ReadModifyWrite'></a>
// Read any supported format into the common feature model.
var collection = GeoConverter.Read("roads.shp");
foreach (var feature in collection)
{
if (feature.Properties.TryGetValue("name", out var name))
{
Console.WriteLine(name);
}
}
// Write it back out as a different format.
GeoConverter.Write(collection, "roads.fgb");
<sup><a href='/src/Tests/Snippets.cs#L15-L29' title='Snippet source file'>snippet source</a> | <a href='#snippet-ReadModifyWrite' title='Start of snippet'>anchor</a></sup>
Build a collection in memory and serialize it:
<a id='snippet-BuildModel'></a>
var collection = new FeatureCollection
{
new Feature(
new Point(new(151.21, -33.87)),
new Dictionary<string, object?> { ["name"] = "Sydney" }),
};
var geoJson = GeoJson.WriteString(collection);
<sup><a href='/src/Tests/Snippets.cs#L34-L43' title='Snippet source file'>snippet source</a> | <a href='#snippet-BuildModel' title='Start of snippet'>anchor</a></sup>
Raster export (PNG)
Render a FeatureCollection to a PNG, clipped to a bounding box, with a software rasterizer and a
hand-written PNG encoder (no third-party dependencies):
<a id='snippet-RenderToPng'></a>
var features = GeoConverter.Read("countries.geojson");
// Render a specific bounding box (min lon, min lat, max lon, max lat) to a PNG.
var options = new RenderOptions
{
Bounds = new Envelope(-10, 35, 30, 60),
Width = 1200,
Height = 900,
};
MapRenderer.RenderPng(features, "europe.png", options);
<sup><a href='/src/Tests/Snippets.cs#L76-L88' title='Snippet source file'>snippet source</a> | <a href='#snippet-RenderToPng' title='Start of snippet'>anchor</a></sup>
RenderOptions controls the extent (Bounds), pixel Width/Height (height is derived from the
aspect ratio when left at 0), Padding, and the Background/Stroke/Fill colors. From the command
line, output a .png and pass --bbox and --size:
geoconvert world.geojson europe.png --bbox -10,35,30,60 --size 1200x900
Projection
The default MapProjection.PlateCarree treats longitude/latitude as planar X/Y with a uniform scale.
It's faithful for small extents near the equator but compresses high-latitude features at world scale.
Switch to MapProjection.WebMercator for the tiled-map layout most users expect (longitude stays
linear, latitude is projected through ln(tan(π/4 + φ/2)) and clamped to ±85.0511° — the cutoff where
the projection blows up at the poles):
<a id='snippet-RenderWebMercator'></a>
var features = GeoConverter.Read("countries.geojson");
// Web Mercator matches the layout of standard web tile maps. Pair it with
// MapRenderer.WebMercatorWorldBounds for the canonical 1:1 square world view; latitude is
// clamped to ±85.0511° (the cutoff every tile provider uses).
var options = new RenderOptions
{
Bounds = MapRenderer.WebMercatorWorldBounds,
Width = 1200,
Projection = MapProjection.WebMercator,
};
MapRenderer.RenderPng(features, "world.png", options);
<sup><a href='/src/Tests/Snippets.cs#L125-L139' title='Snippet source file'>snippet source</a> | <a href='#snippet-RenderWebMercator' title='Start of snippet'>anchor</a></sup>
From the command line, pass --projection:
geoconvert world.geojson world.png --projection web-mercator --size 1200
Anything more exotic (UTM, Lambert Conformal, Albers, polar stereographic, …) is out of scope — the input model is always WGS84, so reprojection has to happen upstream and the renderer is fed already-projected coordinates (it treats X/Y as planar either way).
Compression
Three formats compress their output and let the caller pick the speed/ratio trade-off. All three
default to CompressionLevel.Optimal, so existing callers keep their current output:
| Format | Knob | Default |
|---|---|---|
| PNG | RenderOptions.Compression (deflate level for the IDAT chunk) |
CompressionLevel.Optimal |
| KMZ | Kmz.Write(..., CompressionLevel) (the doc.kml zip entry) |
CompressionLevel.Optimal |
| GeoParquet | GeoParquet.Write(..., ParquetCompression, CompressionLevel) (codec, plus gzip level when the codec is Gzip) |
ParquetCompression.Snappy |
ParquetCompression exposes Snappy, Uncompressed and Gzip on the writer. Zstd is intentionally
not writable — the BCL only ships a Zstd stream decoder — but the GeoParquet reader still accepts
Zstd-encoded pages on .NET 11+.
<a id='snippet-Compression'></a>
// PNG: the deflate level for the IDAT chunk is exposed on RenderOptions.
MapRenderer.RenderPng(
features,
"world.png",
new()
{
Bounds = MapRenderer.WebMercatorWorldBounds,
Projection = MapProjection.WebMercator,
Compression = CompressionLevel.Fastest,
});
// KMZ: the doc.kml zip entry's compression level is an optional Write argument.
using (var kmz = File.Create("world.kmz"))
{
Kmz.Write(kmz, features, CompressionLevel.SmallestSize);
}
// GeoParquet: pick the codec (default Snappy); CompressionLevel only applies to Gzip.
using (var parquet = File.Create("world.parquet"))
{
GeoParquet.Write(parquet, features, ParquetCompression.Gzip, CompressionLevel.SmallestSize);
}
<sup><a href='/src/Tests/Snippets.cs#L95-L120' title='Snippet source file'>snippet source</a> | <a href='#snippet-Compression' title='Start of snippet'>anchor</a></sup>
Exampl generated png
All Australian suburbs
<img src="/src/Tests/PngTests.Render_RealMap.verified.png" height="1100px">
Command line
Installed as a .NET tool named
geoconvert:
geoconvert <input> <output> [--from <format>] [--to <format>]
Formats are detected from the file extensions; --from/--to override that. Examples:
geoconvert cities.geojson cities.kml
geoconvert roads.shp roads.fgb
geoconvert data.csv data.geojson --from csv
Run geoconvert --list to see the supported format names, or geoconvert --help for usage.
Model
Everything reads into and writes out of a FeatureCollection:
FeatureCollection— a named, possibly nested group of features. Has an optionalName, aPropertiesdictionary for layer-level metadata, a list of directFeatures, and a list ofChildrensub-layers (recursive). It'sIEnumerable<Feature>over the whole tree, andCountmatches that enumeration — soforeach (var feature in collection)andcollection.Countalways see every feature regardless of how the tree is shaped.Feature— aGeometryplus a string-keyedPropertiesdictionary and an optionalId.Geometry—Point,LineString,Polygon,MultiPoint,MultiLineString,MultiPolygonorGeometryCollection, built fromPositionvalues (X = longitude, Y = latitude, optional Z and M).
Layered collections
Some formats have a native concept of named sub-layers (KML folders, TopoJSON objects, etc.); the rest are single-layer by spec. Layers are preserved across formats that support them and flattened on write into formats that don't.
| Format | Layer mapping |
|---|---|
| KML / KMZ | <Folder> ↔ FeatureCollection.Children (recursive); folder <name> ↔ Name |
| TopoJSON | each objects entry ↔ one child layer; entry key ↔ Name |
| KMZ (read) | multi-document archives become a root with one child per .kml entry |
| GPX | <wpt>/<rte>/<trk> ↔ children named waypoints/routes/tracks (preserves the wpt/rte/trk distinction across a round trip) |
| Shapefile (directory) | one .shp in the directory ↔ one child layer, named after the filename |
| GeoJSON, FlatGeobuf, GeoParquet, CSV, WKT, WKB | single layer — child layers are flattened on write |
<a id='snippet-Layered'></a>
// A FeatureCollection can hold nested child layers, each with its own Name. Formats with a
// native layer concept (KML folders, TopoJSON objects, KMZ documents, GPX wpt/rte/trk,
// Shapefile bundle directories) round-trip this structure; everything else flattens via the
// recursive enumerator.
var cities = new FeatureCollection { Name = "cities" };
cities.Add(new Feature(new Point(new(151.21, -33.87))));
var roads = new FeatureCollection { Name = "roads" };
roads.Add(new Feature(new LineString([new(151.20, -33.86), new(151.22, -33.88)])));
var root = new FeatureCollection { Name = "sydney" };
root.Children.Add(cities);
root.Children.Add(roads);
GeoConverter.Write(root, "sydney.kml"); // emits <Folder name="cities">… <Folder name="roads">…
// Single-layer formats just flatten — iterating any collection always yields every feature.
foreach (var feature in root)
{
Console.WriteLine(feature.Geometry);
}
<sup><a href='/src/Tests/Snippets.cs#L49-L71' title='Snippet source file'>snippet source</a> | <a href='#snippet-Layered' title='Start of snippet'>anchor</a></sup>
Benchmarks
BenchmarkDotNet benchmarks live in src/Benchmarks and must run in
Release:
dotnet run -c Release --project src/Benchmarks -- --filter "*"
ConvertBenchmarks measures reading and writing a 500-polygon collection through each stream format;
RenderBenchmarks measures PNG rasterization. Add --job Dry for a quick smoke run.
Notes and limitations
- Shapefile holds a single geometry category per file; writing a collection that mixes points,
lines and polygons throws. This is mandated by the format, not a GeoConvert choice — the
.shpheader declares one shape type for the whole file, so a mixed collection has no valid encoding and the consumer must split it into one file per geometry type first. Output is 2D: the format does define Z and M variants, but GeoConvert drops those ordinates rather than emit them. A WGS84.prjis emitted. WhenShapefile.Read/Shapefile.Writeis given a directory (or a path ending in a separator) instead of a.shp, the directory is treated as a bundled dataset: one child layer per.shpon read, one.shpper child on write — the natural shape for ESRI/Natural Earth bundles that ship several shapefiles together. - FlatGeobuf is written without the optional packed R-tree spatial index
(
index_node_size = 0) and is 2D. The index is a query accelerator, not data: it lets a reader fetch features in a bounding box without scanning the whole file, but carries no information the feature records don't. So GeoConvert reads an indexed file by computing the index size and skipping past it — full-file conversion needs every feature anyway — and writes none, leaving output that is still valid FlatGeobuf (GDAL, QGIS and flatgeobuf.org read it fine) for the consumer to re-index on import if it wants spatial queries. Emitting one would mean hand-rolling a Hilbert R-tree to honour the no-dependency rule, which is real complexity for a benefit a conversion tool rarely needs. - GPX reads waypoints, routes and tracks into child layers named
waypoints,routesandtracks— the only way to preserve the wpt/rte/trk distinction across a round trip (geometry type alone doesn't carry it, since both rte and trk are line strings). Writing a flat collection dispatches by geometry type (LineString → trk); writing a layered collection routes each feature back to its original element. GPX has no native area type, so polygons are written as a track with one segment per ring, multi polygons flatten every ring into a single track, and geometry collections write each member geometry in turn. Reading a track with several segments yields a multi line string, so polygons do not survive a round trip as polygons. - KML / KMZ preserve
<Folder>hierarchy as nestedFeatureCollection.Children. A KMZ archive with several.kmlentries reads as a root with one child per document; on write the whole layered tree is stored as a singledoc.kml(multi-document packaging is not reconstructed). - TopoJSON preserves the top-level
objectsdict as child layers (one per entry, keyed byName). The dict is single-level, so grandchildren are flattened into their parent on write. - WKT and WKB carry geometry only — feature attributes are dropped on write.
- GeoParquet is written as a single row group with PLAIN-encoded pages and a flat schema;
geometry is stored as WKB (Z/M preserved) with the CRS defaulting to OGC:CRS84. Page compression
defaults to Snappy and can be switched to
UncompressedorGzip(with a tunableCompressionLevel) via theParquetCompressionoverload ofGeoParquet.Write. The whole Parquet container is hand-rolled to honour the no-dependency rule, so the supported surface is a subset: on read it also handles dictionary encoding and data page V2 (as written by GDAL, DuckDB and pyarrow). Zstd pages are read on .NET 11 builds (where Zstd is part of the BCL) and rejected with a clear error on earlier targets; Zstd is not exposed on the writer. - PNG is a write-only raster export; reading a
.pngthrows. It needs an extent — when noBoundsis given, the full extent of the data is used. - Property values are scalars (
string,long,double,bool); a nested JSON object or array is stored as its raw JSON text in a single string property.
Sample maps for tests
src/Tests/australian_suburbs.geojson— sourced from https://github.com/anthwri/GeoJson-Data.src/Tests/world.geojson— Natural Earth 1:110m Admin 0 Countries, public domain. Downloaded from https://github.com/nvkelso/natural-earth-vector/blob/master/geojson/ne_110m_admin_0_countries.geojson.
Icon
Pattern designed by Kim Sun Young from The Noun Project.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (3)
Showing the top 3 NuGet packages that depend on GeoConvert:
| Package | Downloads |
|---|---|
|
MapBundle
Core runtime for MapBundle. Loads bundled map data (borders, cities, waterways) that ships as FlatGeobuf in the MapBundle.World and MapBundle.[Region] packages. Data is derived from OpenStreetMap (© OpenStreetMap contributors, ODbL). |
|
|
GeoConvert.Skia
Optional SkiaSharp-backed PNG render backend for GeoConvert. Rasterizes the same projection, styling and labelling pipeline as the built-in renderer through Skia. Unlike GeoConvert itself, this package takes a third-party dependency (SkiaSharp). |
|
|
GeoConvert.ImageSharp
Optional SixLabors.ImageSharp-backed PNG render backend for GeoConvert. Rasterizes the same projection, styling and labelling pipeline as the built-in renderer through ImageSharp. Unlike GeoConvert itself, this package takes a third-party dependency (SixLabors.ImageSharp, under the Six Labors Split License). |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.1 | 11 | 6/8/2026 |
| 1.1.0 | 36 | 6/8/2026 |
| 1.0.1 | 337 | 6/6/2026 |
| 1.0.0 | 45 | 6/6/2026 |
| 0.12.0 | 43 | 6/5/2026 |
| 0.11.0 | 1,498 | 6/3/2026 |
| 0.10.1 | 904 | 6/3/2026 |
| 0.10.0 | 894 | 6/2/2026 |
| 0.9.1 | 899 | 6/2/2026 |
| 0.9.0 | 900 | 6/2/2026 |
| 0.8.2 | 903 | 6/2/2026 |
| 0.8.1 | 87 | 6/2/2026 |
| 0.8.0 | 92 | 6/1/2026 |
| 0.6.0 | 109 | 5/30/2026 |
| 0.5.1 | 115 | 5/28/2026 |
| 0.5.0 | 105 | 5/28/2026 |
| 0.4.0 | 103 | 5/28/2026 |
| 0.3.0 | 96 | 5/27/2026 |
| 0.2.0 | 90 | 5/27/2026 |
| 0.1.9 | 109 | 5/26/2026 |