# How the geolibre-wasm Runtime Manages Raster I/O and Conversions for PMTiles and COG

> Discover how the geolibre-wasm runtime efficiently handles raster I/O and conversions for PMTiles and COG using WebAssembly bundles for client-side processing.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-08-04

---

**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](https://github.com/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`](https://github.com/opengeos/GeoLibre/blob/main/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:

```typescript
// 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:

1. **Extract metadata** with `geotiff_info` → `readGeoTiffInfo()` (L85-L92) to read width, height, band count, and georeferencing
2. **Instantiate `CogBuilder`** with source dimensions
3. **Select compression** from bindgen-supported codecs: `deflate`, `lzw`, `packbits`, or `none` (L38-L47)
4. **Feed raw bytes** to produce a tiled COG with 512px default tile size and internal overviews (L33-L36)
5. **Return the encoded `Uint8Array`** for direct map loading

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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_RETRIES` attempts and `RETRY_DELAY_MS` backoff
- **Abort support** via `AbortSignal` for cancellation
- **Safety guard**: rejects responses >64 MiB from servers ignoring Range headers (L76-L80)

```typescript
// 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.

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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):

```typescript
// 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)

```typescript
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:

1. **COG conversion panel**: Plain GeoTIFF uploads route through `CogBuilder` to produce tiled layers
2. **PMTiles drag-and-drop**: Files trigger `extractPmtiles()` for selective range extraction
3. **Raster subset dialog**: Calls appropriate functions from [`raster-subset.ts`](https://github.com/opengeos/GeoLibre/blob/main/raster-subset.ts) based 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`](https://github.com/opengeos/GeoLibre/blob/main/cog-convert.ts#L38-L47): **`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.