GeoConvert 0.3.0
See the version list below for details.
dotnet add package GeoConvert --version 0.3.0
NuGet\Install-Package GeoConvert -Version 0.3.0
<PackageReference Include="GeoConvert" Version="0.3.0" />
<PackageVersion Include="GeoConvert" Version="0.3.0" />
<PackageReference Include="GeoConvert" />
paket add GeoConvert --version 0.3.0
#r "nuget: GeoConvert, 0.3.0"
#:package GeoConvert@0.3.0
#addin nuget:?package=GeoConvert&version=0.3.0
#tool nuget:?package=GeoConvert&version=0.3.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-L12' 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#L17-L33' 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#L38-L52' 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#L97-L111' 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
When RenderOptions.Projection is left at its default MapProjection.Auto, the renderer picks one from the data bounds: a regional extent (latitude span < 60°, longitude span < 90°) renders as Lambert Conformal Conic, a continental extent renders as plate carrée, and a world extent (longitude span ≥ 180° or latitude span ≥ 90°) renders as Goode's Homolosine — equal-area, so high-latitude landmasses read at honest size. Auto never picks Web Mercator — that's a deliberate layout choice (tile-style), not a distortion-minimisation one, so it stays explicit.
| Region | lon span | lat span | Auto picks |
|---|---|---|---|
| Contiguous USA | 60° | 25° | Lambert |
| France | 15° | 10° | Lambert |
| Europe | 40° | 35° | Lambert |
| Australia | 41° | 34° | Lambert |
| Africa | 75° | 73° | PlateCarree (latSpan ≥ 60°) |
| Asia | 165° | 80° | PlateCarree |
| World | 360° | 180° | Goode |
To override, set Projection directly. MapProjection.PlateCarree treats longitude/latitude as planar X/Y with a uniform scale — cheap and faithful for small equatorial extents but compresses high-latitude features at world scale. MapProjection.WebMercator produces the tiled-map layout most callers will recognise (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,
Projection = MapProjection.WebMercator,
};
MapRenderer.RenderPng(features, "world.png", options);
<sup><a href='/src/Tests/Snippets.cs#L234-L249' 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
For a single country, state, or province where neither plate carrée's high-latitude squish nor Web Mercator's pole stretch is acceptable, MapProjection.Lambert is the right pick — also what Auto selects under the covers for bounds of this shape. It's spherical Lambert Conformal Conic with two standard parallels auto-picked at 1/6 and 5/6 of the data's latitude range (the de facto convention for country-scale layouts), conformal, and keeps area distortion low across a region a few hundred to a couple thousand kilometres wide. Outside that scale it degenerates (the cone flattens at the equator if bounds are vertically symmetric, in which case the renderer falls back to plate carrée), so it isn't a world projection — pair it with regional Bounds:
<a id='snippet-RenderLambert'></a>
var features = GeoConverter.Read("states.geojson");
// Lambert Conformal Conic with standard parallels picked from the data bounds — the textbook
// choice for state/country-scale maps. Conformal and low-distortion across a regional extent,
// so this avoids both plate-carrée's high-latitude squish and Web Mercator's pole stretch.
var options = new RenderOptions
{
Projection = MapProjection.Lambert,
};
MapRenderer.RenderPng(features, "states.png", options);
<sup><a href='/src/Tests/Snippets.cs#L254-L268' title='Snippet source file'>snippet source</a> | <a href='#snippet-RenderLambert' title='Start of snippet'>anchor</a></sup>
geoconvert states.geojson states.png --projection lambert --size 1600
For a world map, MapProjection.Goode is what Auto picks under the covers — and what the explicit setting selects. It's the interrupted form of Goode's Homolosine with a Greenland cut-out: the world is split into two northern and four southern lobes that meet along ocean meridians (-40° in the north; -100°, -20° and +80° in the south), and the northern cut steps east to lon=-10° above lat=60° so Greenland (and Iceland) render adjacent to Canada in the Americas lobe rather than being bisected at -40°. Inside each lobe the projection is the classic Homolosine — sinusoidal between ±40°44'11.8" and Mollweide outside that band. The interruptions absorb the distortion that would otherwise pile up at the lobe edges, so the major continents stay intact and the projection is equal-area: Greenland reads at honest size relative to Africa, unlike under plate carrée or Web Mercator. Polygons that straddle a lobe boundary are clipped with Sutherland-Hodgman before projection so each lobe's contribution closes along the clip meridian, and polylines are split at the boundaries; Antarctica falls inside the four southern lobes and reads as four separate pieces along the bottom of the map, which is the visual signature of the projection. Setting RenderOptions.Ocean paints each lobe with that colour before the continents render, so the lobed shape (and the inter-lobe gaps) pops visually.
<a id='snippet-RenderGoode'></a>
var features = GeoConverter.Read("countries.geojson");
// Goode's Homolosine (interrupted into 2 northern and 4 southern lobes along ocean
// meridians, the conventional layout): equal-area, so areas at high latitudes don't blow
// up like they do under Web Mercator or compress like they do under plate carrée, and the
// lobe interrupts keep distortion low on every continent. This is what MapProjection.Auto
// picks for a world map, so the explicit Projection assignment is only needed when you
// want the specific extent — leaving it off and letting Auto pick produces the same result.
// Ocean fills each lobe under the continents so the projection's lobed shape (and the
// inter-lobe gaps) reads clearly.
var options = new RenderOptions
{
Projection = MapProjection.Goode,
};
MapRenderer.RenderPng(features, "world.png", options);
<sup><a href='/src/Tests/Snippets.cs#L273-L292' title='Snippet source file'>snippet source</a> | <a href='#snippet-RenderGoode' title='Start of snippet'>anchor</a></sup>
geoconvert world.geojson world.png --projection goode --size 1600
Anything more exotic (UTM, Albers Equal-Area, 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).
Per-layer styling
When the input has nested sub-layers (see layered collections) the renderer walks the tree depth-first, so a parent layer paints under its children — features added deeper in the tree appear on top in source-over blending. RenderOptions.LayerStyle is a callback that returns a LayerStyle for any given layer; any property left null inherits its default from RenderOptions, so a partial override (only a fill, or only a stroke width) doesn't have to repeat the other knobs.
<a id='snippet-RenderLayers'></a>
// A FeatureCollection with named sub-layers — the renderer walks the tree depth-first, so a
// parent layer paints under its children. RenderOptions.LayerStyle picks per-layer colors;
// any property left null falls back to the defaults on RenderOptions.
var basemap = new FeatureCollection
{
Name = "basemap"
};
basemap.Add(
new Feature(
new Polygon(
[
[new(-10, 35), new(30, 35), new(30, 60), new(-10, 60), new(-10, 35)],
])));
var roads = new FeatureCollection
{
Name = "roads"
};
roads.Add(new Feature(new LineString([new(0, 40), new(20, 55)])));
basemap.Children.Add(roads);
var options = new RenderOptions
{
Bounds = new Envelope(-10, 35, 30, 60),
LayerStyle = layer => layer.Name switch
{
"basemap" => new()
{
Fill = new(230, 230, 230),
Stroke = new(180, 180, 180),
},
"roads" => new()
{
Stroke = new(200, 60, 60),
StrokeWidth = 3,
},
_ => null,
},
};
MapRenderer.RenderPng(basemap, "europe.png", options);
<sup><a href='/src/Tests/Snippets.cs#L116-L160' title='Snippet source file'>snippet source</a> | <a href='#snippet-RenderLayers' title='Start of snippet'>anchor</a></sup>
When the layers come from independent sources (typically a basemap file plus an overlay file), pass the collections as a list — they render in order, first under, last on top. Each FeatureCollection is a top-level layer for RenderOptions.LayerStyle, and the rendered extent defaults to the union of every input's bounds:
<a id='snippet-RenderStackedCollections'></a>
// When the layers come from independent sources (a basemap file plus an overlay file, say),
// pass them as a list — they render in order, first under, last on top. Each FeatureCollection
// is a top-level layer for RenderOptions.LayerStyle, so giving each one a Name is enough to
// style them distinctly. When Bounds is null the rendered extent is the union of every input.
var basemap = GeoConverter.Read("countries.geojson");
basemap.Name = "basemap";
var roads = GeoConverter.Read("roads.shp");
roads.Name = "roads";
var options = new RenderOptions
{
LayerStyle = layer => layer.Name switch
{
"basemap" => new()
{
Fill = new(230, 230, 230),
Stroke = new(180, 180, 180),
},
"roads" => new()
{
Stroke = new(200, 60, 60),
StrokeWidth = 3,
},
_ => null,
},
};
MapRenderer.RenderPng([basemap, roads], "stacked.png", options);
<sup><a href='/src/Tests/Snippets.cs#L165-L197' title='Snippet source file'>snippet source</a> | <a href='#snippet-RenderStackedCollections' title='Start of snippet'>anchor</a></sup>
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#L204-L229' title='Snippet source file'>snippet source</a> | <a href='#snippet-Compression' title='Start of snippet'>anchor</a></sup>
Example 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#L59-L92' 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 inRelease:
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 |