How the geolibre-wasm Runtime Manages Raster I/O and Conversions for PMTiles and COG
The geolibre-wasm runtime uses two dedicated WebAssembly bundles—one wasm-bindgen module for high-level COG conversion and PMTiles planning, plus a WASI toolchain for on-the-fly raster sub-setting—to perform all client-side raster processing without server dependencies.
The geolibre-wasm runtime in opengeos/GeoLibre powers raster workflows entirely in the browser. It splits heavy processing between a lean bindgen module for COG creation and PMTiles extraction, and a lazily-loaded WASI bundle for raster sub-setting from remote sources. This dual-bundle architecture keeps initial load times minimal while enabling complex geospatial operations on any deployment target—web, Tauri desktop, or embedded Jupyter.
WebAssembly Bundle Architecture
The runtime ships as two independent bundles that handle different raster I/O concerns:
| Bundle | Export Type | Primary Capabilities |
|---|---|---|
geolibre-wasm (wasm-bindgen) |
CogBuilder, GeoTiffReader, geotiff_info, PmtilesExtractor |
GeoTIFF → COG conversion, PMTiles range planning |
geolibre-wasm/tools (WASI runner) |
extractCogSubset, extractWmsSubset, extractXyzTileSubset |
On-demand raster sub-setting from COG, WMS, and XYZ tile sources |
The bindgen module loads immediately via import … from "geolibre-wasm", while the WASI tools are fetched on-demand with import("geolibre-wasm/tools"). This separation ensures that heavy processing payloads only download when users trigger advanced operations.
GeoTIFF to COG Conversion with CogBuilder
The CogBuilder class in packages/processing/src/cog-convert.ts handles the core conversion pipeline. Written in Rust and exposed through wasm-bindgen, it transforms plain GeoTIFFs into tiled, overview-enhanced Cloud-Optimized GeoTIFFs.
Initialization and Caching
The module initializes through initCogWasm(), which memoizes the compiled instance across calls. Failures automatically clear the cache, allowing clean retries:
// From packages/processing/src/cog-convert.ts (L58-L81)
let wasmInstance: CogBuilderModule | null = null;
export async function initCogWasm(): Promise<void> {
if (wasmInstance) return;
try {
wasmInstance = await init();
} catch (err) {
wasmInstance = null; // Clear cache on failure
throw err;
}
}
Conversion Pipeline
The five-step process operates entirely on Uint8Array buffers in memory:
- Extract metadata with
geotiff_info→readGeoTiffInfo()(L85-L92) to read width, height, band count, and georeferencing - Instantiate
CogBuilderwith source dimensions - Select compression from bindgen-supported codecs:
deflate,lzw,packbits, ornone(L38-L47) - Feed raw bytes to produce a tiled COG with 512px default tile size and internal overviews (L33-L36)
- Return the encoded
Uint8Arrayfor direct map loading
import { initCogWasm, CogBuilder, readGeoTiffInfo } from "./cog-convert";
async function geoTiffToCog(
bytes: Uint8Array,
compression: "deflate" | "lzw" | "packbits" | "none" = "deflate"
): Promise<Uint8Array> {
await initCogWasm();
const info = await readGeoTiffInfo(bytes);
const builder = new CogBuilder(info.width, info.height, info.bands);
return builder.encode(bytes, { compression });
}
The complete in-memory processing eliminates Python dependencies, making this workflow universal across all GeoLibre deployment targets.
PMTiles Extraction with Sans-IO Planning
The PmtilesExtractor in packages/processing/src/pmtiles-extract.ts implements a sans-IO architecture: the Rust side performs pure logic to determine required byte ranges, while JavaScript handles all network I/O.
Range Planning and HTTP Coordination
For a given bounding box and zoom range, the extractor emits a list of byte ranges to fetch. The JavaScript glue code manages:
- Concurrent downloads (default: 8 parallel requests)
- Retry logic with
RANGE_RETRIESattempts andRETRY_DELAY_MSbackoff - Abort support via
AbortSignalfor cancellation - Safety guard: rejects responses >64 MiB from servers ignoring Range headers (L76-L80)
// From packages/processing/src/pmtiles-extract.ts (L64-L69)
export interface ExtractResult {
archive: Uint8Array; // Complete, self-contained .pmtiles file
source: SourceMetadata; // Zoom range, bounds, tile type
stats: TransferStats; // Bytes transferred, tiles extracted, timing
}
Shared Runtime Initialization
The PMTiles module reuses the same initCogWasm() initializer as COG conversion (L13-L14), ensuring the WebAssembly payload loads only once regardless of which raster pipeline activates first.
import { extractPmtiles } from "./pmtiles-extract";
async function extractPartialPmtiles(
url: string,
bbox: [number, number, number, number],
zoomRange?: [number, number]
): Promise<Uint8Array> {
const result = await extractPmtiles(url, { bbox, zoomRange });
return result.archive; // Ready-to-load PMTiles archive
}
Raster Sub-Setting via WASI Tools
The geolibre-wasm/tools bundle provides lightweight WASI binaries for extracting raster subsets without full file downloads. The wrapper in packages/processing/src/raster-subset.ts exposes three core functions:
| Function | Source Type | Output |
|---|---|---|
extractCogSubset |
Cloud-Optimized GeoTIFF URL | COG Uint8Array |
extractWmsSubset |
WMS endpoint | COG Uint8Array |
extractXyzTileSubset |
XYZ tile template | COG Uint8Array |
Lazy Loading and Error Handling
The wrapper delays loading the ~multi-megabyte WASI bundle until first use via loadSubsetModule(), with automatic retry on transient network failures (L34-L42):
// From packages/processing/src/raster-subset.ts (L44-L52)
export async function extractCogSubset(
url: string,
options: SubsetOptions
): Promise<Uint8Array> {
const module = await loadSubsetModule();
// WASI tool performs byte-range reads internally
const result = module.extractCogSubset(url, JSON.stringify(options));
return result;
}
Sub-Setting Strategies
Each extractor produces a valid COG that MapLibre can render immediately:
- COG subset: Identifies and fetches only tiles intersecting the requested bbox, returning a new, smaller COG (L44-L52)
- WMS subset: Issues optimized GetMap request(s) and wraps the response in COG structure (L62-L68)
- XYZ subset: Downloads, mosaics, and re-encodes raster tiles from an XYZ template into a unified COG (L78-L85)
import { extractCogSubset, extractXyzTileSubset } from "./raster-subset";
// Extract from a remote COG
const cogSubset = await extractCogSubset(
"https://example.com/imagery.tif",
{ bbox: [-120, 35, -119, 36], width: 1024, height: 1024 }
);
// Extract from XYZ tiles
const xyzSubset = await extractXyzTileSubset(
"https://tileserver/{z}/{x}/{y}.png",
{ bbox: [-120, 35, -119, 36], zoom: 14 }
);
UI Integration and Runtime Lifecycle
The GeoLibre UI orchestrates these raster pipelines through three primary entry points:
- COG conversion panel: Plain GeoTIFF uploads route through
CogBuilderto produce tiled layers - PMTiles drag-and-drop: Files trigger
extractPmtiles()for selective range extraction - Raster subset dialog: Calls appropriate functions from
raster-subset.tsbased on source type
All runtime initializations use memoized promises (wasmReady, subsetModulePromise) to guarantee single-load semantics. Failed initializations reset these promises, enabling clean retry behavior without page refresh.
Summary
The geolibre-wasm runtime manages raster I/O and conversions through a carefully layered architecture:
- Dual-bundle loading separates immediate needs (bindgen) from heavy processing (WASI tools) to optimize bundle sizes
- Sans-IO design in Rust keeps the WebAssembly side pure—network concerns stay in JavaScript with full control over concurrency, retries, and cancellation
- Shared initialization between COG and PMTiles pipelines prevents duplicate WebAssembly compilation
- Universal COG output from all sub-setting tools enables consistent MapLibre integration regardless of source format
- Zero server dependencies enable offline-capable raster workflows across web, desktop, and embedded environments
Frequently Asked Questions
How does geolibre-wasm handle large files without running out of memory?
The runtime uses streaming range requests for PMTiles and tile-aware subsetting for COGs, fetching only the data regions required for the current view. For full-file conversion, the source is copied into memory once—practical for typical browser limits (hundreds of MB) but not multi-GB archives. The WASI subset tools avoid this by working with remote URLs rather than loading complete files.
What compression codecs are supported for COG output?
The bindgen module supports four codecs as defined in cog-convert.ts: deflate (default), lzw, packbits, and none. These match the codecs compiled into the Rust encoder. The UI exposes these as user-selectable options during conversion.
Can I use the PMTiles extractor with servers that don't support HTTP Range requests?
No—the extractor requires Range support to function. The JavaScript glue includes a safety check (L76-L80) that rejects responses larger than 64 MiB, catching servers that ignore Range headers and return full archives. For such servers, pre-downloading the complete PMTiles file and loading it locally is the recommended fallback.
Why are the WASI tools in a separate bundle instead of included in the main wasm module?
Lazy loading keeps the initial JavaScript payload small. The bindgen module (~hundreds of KB) loads immediately for core COG and PMTiles operations, while the WASI tools (~multiple MB) fetch only when users invoke advanced sub-setting features. This split is particularly important for the Tauri desktop build and mobile web scenarios where bandwidth and startup time matter.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →