Skip to content

OxiGeo 0.2.4 Release

Latest

Choose a tag to compare

@cool-japan cool-japan released this 18 Aug 11:32

[0.2.4] - 2026-08-18

Added

  • oxigeo-proj: added the unambiguous type aliases SphericalTransverseMercator (= TransverseMercator) and EllipsoidalTransverseMercator (= GaussKruger), re-exported at the crate root, so call sites can state which Earth model they mean — TransverseMercator is sphere-based and is wrong for UTM/national grids by ~24.9 km of northing at 48° N, which its docs now warn about prominently; a new regression test pins the two apart at a real UTM 33N reference point.
  • oxigeo-proj: re-exported the ellipsoidal Transverse Mercator kernel projections::tmerc_forward / projections::tmerc_inverse from projections (previously reachable only as projections::cylindrical::tmerc_*).
  • oxigeo-proj: transform now re-exports SphericalTransverseMercator and EllipsoidalTransverseMercator alongside CassineSoldner/GaussKruger/TransverseMercator, so use oxigeo_proj::transform::* surfaces the two aliases instead of forcing the longer transform::cylindrical:: path. Same std gate as the existing re-exports; a regression test now imports them through the glob and checks they denote the same types as the crate-root re-exports.
  • oxigeo-geoparquet: GeoParquetReader::from_bytes(impl Into<bytes::Bytes>) reads a GeoParquet image held entirely in memory. The reader now keeps an internal File/Bytes source that implements ChunkReader, so every read path — read_geometries, read_row_group, read_all, read_pushdown — behaves identically for on-disk and in-memory inputs, with no change to GeoParquetReader's public shape.
  • oxigeo-geoparquet: GeoParquetReader::read_geometries_optional(row_group) and GeoParquetBatchReader::extract_geometries_optional(batch) return Vec<Option<Geometry>> with exactly one entry per row, so geometries stay index-aligned with their property rows; GeoParquetBatchReader::geometry_encoding() exposes the geometry column's declared encoding.
  • oxigeo-geotiff: tiff::is_mask_ifd(&Ifd, ByteOrderType) classifies a directory as a GDAL internal (transparency) mask, with the pure core tiff::is_mask_markers(new_subfile_type, photometric) and the marker constants tiff::SUBFILE_TYPE_TRANSPARENCY_MASK / tiff::PHOTOMETRIC_TRANSPARENCY_MASK.
  • oxigeo-geotiff: CogReader::ifd_count(), CogReader::level_ifd(level) and CogReader::level_ifd_index(level) expose the level → IFD mapping and the raw chain length, so a consumer that wants the mask IFDs — or wants to know how many non-level IFDs a file carries — can still reach them while the level API stays mask-free.
  • oxigeo-geotiff: CogReader::tile_pixel_size(level, tile_y) returns the decoded pixel dimensions of the block read_tile produces at that level — the level's own TileWidth/TileLength, or ImageWidth × RowsPerStrip narrowed for the short final strip — so a caller can size an image buffer that cannot disagree with the bytes it gets.
  • oxigeo-gpkg: GeoPackage::scan_table_by_name_typed(table) scans a table like scan_table_by_name but applies SQLite's REAL type affinity to the result: SQLite stores a lossless 40.0 in a REAL/DOUBLE/FLOAT-declared column as the integer 40, so an untyped scan surfaces it as an Integer — the typed variant restores every such value to the equivalent Float (via restore_real_affinity, driven by the declared column types), so 40 and 40.0 read back identically, matching what every affinity-aware SQLite consumer sees. scan_table_by_name itself is unchanged and still returns raw storage classes.

Changed

  • Renamed the workspace quick-xml dependency (Cargo.toml) to the oxixml-quickxml-compat package (drop-in quick-xml 0.41 compatible shim), keeping the local dependency name quick-xml so every consuming crate (oxigeo-drivers-advanced, oxigeo-vrt, oxigeo-services, oxigeo-server, oxigeo-metadata, oxigeo-qc) required no source changes.
  • deny.toml: added a quick-xml entry to [bans].deny, scoped with wrappers = ["inferno"] for the one remaining transitive path (inferno -> pprof -> oxigeo-algorithms's dev-only pprof dependency); the direct-consumer graph is clear (cargo tree -i quick-xml -e normal --workspace is empty).
  • oxigeo-gpkg: gated GeoJSON conversion (vector::geojson_convert, and its oxigeo-geojson-stream/serde_json dependencies) behind a new geojson-convert feature, kept in default so no existing build breaks; cargo build --no-default-features (e.g. for wasm) no longer pulls in the regex family via oxigeo-geojson-stream. Consumers that already build oxigeo-gpkg with default-features = false will need to add features = ["geojson-convert"] to keep using vector::geojson_convert.
  • oxigeo-proj: the oxiproj dependency is now optional and pulled in by the std feature instead of being unconditional. Every OxiProj call site already lived in a std-gated module (transform, pipeline, projections, …), so a --no-default-features (no_std + alloc) build was compiling OxiProj purely as dead weight; cargo tree -p oxigeo-proj --no-default-features -e normal now lists only byteorder, serde and thiserror. default = ["std"] is unchanged, so the default public surface is byte-identical and the 13 in-workspace dependents (all of which use default features or ask for std explicitly) need no change. Migration: two trait impls — impl From<oxiproj::TransformError> for Error and impl From<oxiproj::ProjError> for Error — are now #[cfg(feature = "std")] and therefore absent from --no-default-features builds; they could not have been used there anyway, since the oxiproj types they convert from were not linked. No Error variant changed: all of them carry String, not OxiProj types. --no-default-features --features proj-db remains unsupported (it was already failing to compile before this change, for unrelated alloc prelude reasons in epsg::proj_db) — superseded later in this same release: proj-db now implies std and compiles, see Fixed below.
  • Dependency bumps: oxiproj 0.1.5 — the OxiProj authority-path correctness release, which fixes upstream the divergent EPSG authority definitions documented in the proj-db feature-invariance entry under Fixed (unit-converted ellipsoid axes, method-aware +lat_ts mapping, LCC 1SP, WGS 84-hub datum composition, prime-meridian datum chains, Molodensky-Badekas operations, PROJ's ballpark/fallback selection policy, and grid direction under PROJ_DATA) — plus routine COOLJAPAN ecosystem bumps (oxiarc, oxicode, oxih5, oxionnx, oxisql, oxistore, oxitls; the quick-xmloxixml-quickxml-compat migration has its own entry above).
  • oxigeo-gpkg: SqliteHeader gained the public field reserved_bytes: u8 (byte 20 of the SQLite database header — bytes reserved at the end of every page) and a usable_size() helper; the issue #17 overflow-page fix (see Fixed) computes local-payload thresholds from the usable page size, not the raw page size. Compatibility note: constructing SqliteHeader with a struct literal outside the crate now requires the extra field; code that obtains headers through SqliteReader is unaffected.
  • oxigeo-wasm: WasmCogViewer, AdvancedCogViewer and BatchTileLoader hold their cached parsed reader (see the reader-reuse fix under Fixed) in Rc/RefCell and therefore no longer implement Send/Sync. On wasm32-unknown-unknown — the target these #[wasm_bindgen] types exist for — this is inert (single-threaded, driven from JS); only a non-wasm caller holding one behind a Send/Sync bound would notice, and none exists in the workspace.

Fixed

  • oxigeo-geoparquet: GeoParquetBatchReader::extract_geometries now dispatches on the geometry column's declared encoding instead of downcasting to BinaryArray unconditionally — a GeoArrow-native file read through read_all() / next_batch() previously failed with a type_mismatch error rather than decoding.
  • oxigeo-geoparquet: null geometries no longer silently desynchronise geometries from their property rows — the new read_geometries_optional / extract_geometries_optional variants keep each null as a None at its original index (the existing null-dropping methods are unchanged).
  • oxigeo-wasm: GDAL internal-mask IFDs (NewSubfileType bit 2, or PhotometricInterpretation == 4) are no longer counted as overview levels by the browser COG reader — they share the IFD chain with the overviews, so overviewCount was inflated and every level index past the first mask was shifted onto the wrong resolution. The chain is still walked through masks, so overviews stored after one are found.
  • oxigeo-wasm: WasmCogViewer.readTile(level, x, y) now honours its level argument on the URL path; it previously called a level-0 shortcut, so every overview request silently re-read full-resolution tiles.
  • oxigeo-wasm: WasmCogViewer and AdvancedCogViewer parse the COG once and reuse the reader across tile reads instead of re-opening the file (HEAD request plus a range request per IFD) on every tile — for AdvancedCogViewer that happened on every tile-cache miss. The cached reader is keyed by URL, so re-opening a different file never serves stale tiles, and a failed open is retried on the next call.
  • oxigeo-wasm: the URL-backed COG path now normalises ModelPixelScaleTag (33550) Y to its magnitude, so WasmCogViewer.pixelScaleY(), the pixelScaleY key of the metadata JSON, and the Rust pixel_scale_y field of CogMetadata / IfdMetadata are never negative. The GeoTIFF spec defines the tag as strictly positive and conforming writers (GDAL included) store it that way, but a few nonconforming writers bake the north-up sign into it; the URL path previously passed that negative value straight through while the openBytes path already applied .abs(), so the same raster reported opposite signs depending on how it was loaded. Neither path builds a pixel-to-CRS affine transform, so applying the north-up sign when constructing one remains the consumer's responsibility — callers that were compensating for the negative value on the URL path must drop that compensation.
  • oxigeo-geotiff: CogReader no longer treats GDAL internal masks as pyramid levels. A mask (NewSubfileType bit 2, or PhotometricInterpretation == 4) shares the IFD chain with the overviews, so overview_count() counted one extra level per mask and every level index past a mask named the wrong resolution: read_tile(2, …) on a [full, overview, mask, overview] chain returned the mask's pixels, and GeoTiffReader::level_size(2) the mask's dimensions. Levels are now mapped onto non-mask IFDs and every level-indexed path — the block-offset cache, the tile_byte_range fallback, band_read::LevelGeometry (window/band reads) and GeoTiffReader::level_size/read_window — resolves through that one map, so the geometry and the tile offsets can no longer describe different images. The same map fixes a latent desync of its own: an IFD whose ImageInfo failed to parse was already skipped when counting overviews but not when indexing tile offsets. Behaviour change: on a masked COG CogReader::overview_count(), GeoTiffReader::level_size, cog::get_cog_info's overview_count and every level argument now describe resolutions only — code that compensated for the inflated count (e.g. by subtracting mask IFDs, or by reading level n+1 to get overview n) must drop that compensation. Raw chain access is unchanged: TiffFile::ifds / image_count() still see every IFD, and CogReader::ifd_count() / level_ifd_index() expose the mapping.
  • oxigeo-wasm: AdvancedCogViewer and WasmCogViewer now report the same overviewCount for the same file. AdvancedCogViewer.open() derived it from TiffFile::image_count(), which counts every IFD including GDAL internal masks, while its own readTile indexed CogReader's mask-free levels and WasmCogViewer skipped masks when walking the chain — so on a masked COG the advanced viewer advertised a level its own tile reads rejected. It now takes the count from the CogReader it reads through. open() also parses the file once instead of twice (it built a whole TiffFile for metadata and then re-opened a CogReader on the first tile read); the reader it parses is handed straight to the tile path.
  • oxigeo-wasm: readTileAsImageData, readTileWithContrast, computeStats and computeHistogram size their RGBA buffer from the requested level's tile geometry instead of the full-resolution tileWidth/tileHeight captured at open(). Now that tile reads honour their level argument, a COG whose overviews declare a different TileWidth/TileLength (gdaladdo is free to choose one) produced an ImageData at the wrong dimensions with the tile truncated or three-quarters transparent. Both viewers convert through one shared helper, so they cannot drift apart again. Each path takes the geometry from the very reader that decodes the block — CogReader::tile_pixel_size for openBytes/AdvancedCogViewer (which narrows for the short final strip of a striped level), the URL reader's own per-level record for WasmCogViewer — so buffer and bytes always agree.
  • oxigeo-wasm: AdvancedCogViewer.open() works in a browser for the first time. The viewer parses the COG with oxigeo_geotiff::CogReader, which reads through the synchronous DataSource trait, but the only data source it had was FetchBackend, whose read_range is hard-wired to NotSupported("Synchronous read in WASM - use async methods") — WASM cannot block on fetch() — and which holds no bytes of its own. Every open() therefore failed on the parser's very first header read, and so did every tile read behind it (readTileCached, readTileAsImageData, readTileWithContrast, computeStats, computeHistogram, BatchTileLoader): the whole URL path was dead code in the one environment it exists for. A new crate-private buffered_source module inverts the loop instead of making the parser async: BufferedRangeSource implements DataSource over a cache of already-downloaded ranges and records the ranges it cannot serve, and pull_until_ready re-runs the synchronous operation, fetching the recorded ranges between attempts, until it completes with nothing pending. Fetches are rounded up to 64 KiB and coalesced, so a normally laid-out COG opens in one HEAD plus one range request and a tile read costs at most one more (none at all when its block is already buffered); a server that ignores Range and answers 200 with the whole body is detected from the status and serves everything from that body thereafter. The loop is keyed on the miss log, not on the error: CogReader::open reads overview ImageInfos, the GeoKey directory and the per-level block index best-effort and swallows the failure, so a driver that retried only on Err would have returned a reader that silently dropped an overview or lost the file's epsgCode. Termination is bounded in every direction — a transport error and a genuine format error are surfaced as themselves, a round that downloads nothing new stops with a "made no progress" error, and the round count is capped. AdvancedCogViewer now keeps the reader, its buffer and its transport together for the life of the opened URL, so tile reads reuse everything the header walk downloaded. The openBytes (in-memory) path is untouched. Native tests drive the whole loop over synthetic TIFF bytes with an in-memory transport; only the web_sys-backed implementation of the fetch seam — a thin translation of one fetch() response, with the response decoding split out and tested — is browser-only.
  • oxigeo-wasm: the pyramid block of AdvancedCogViewer.getMetadata() no longer contradicts the overviewCount printed beside it. It was built from a TilePyramid synthesised from the image's dimensions alone — halving width and height until a single tile remained — which describes a pyramid the file need not contain: a 4096x4096 COG with 256-pixel tiles and no overviews reported numLevels: 5 next to overviewCount: 0, and every level past 0 named tile grids that the viewer's own tile reads reject. The block is now derived from the levels the file actually has — the same mask-filtered IFD chain overviewCount comes from — with each level's own dimensions and block size read back through CogReader::level_ifd. Value changes (keys are unchanged): numLevels is now always overviewCount + 1; tilesPerLevel has one [tilesX, tilesY] entry per real level, computed from that level's own ImageWidth/ImageLength and TileWidth/TileLength (for a striped level, image width by RowsPerStrip) instead of from repeatedly halved level-0 dimensions; totalTiles is the sum over those real levels, and remains a count of spatial blocks — a planar (PlanarConfiguration = 2) file stores SamplesPerPixel times as many. One key is added: pyramid.levels, an array of {width, height, tileWidth, tileHeight, tilesX, tilesY} in level order. TilePyramid itself is unchanged, still exported and still the type to use for tile-scheme math. WasmCogViewer.metadataJson() emits no pyramid block and is unaffected.
  • oxigeo-proj: --no-default-features --features proj-db compiles for the first time (it produced 56 errors before, so no build could ever have depended on its previous behaviour). proj-db now implies std: the feature is not expressible on a no_std + alloc build, because epsg::proj_db opens a file-system database (std::path::{Path, PathBuf}, std::env::var for PROJ_DATA/PROJ_LIB) and drives oxisql-sqlite-compat's async engine through its blocking API on a current_thread tokio runtime. The alternative — sprinkling alloc prelude imports over epsg/proj_db.rs — would only have moved the 56 errors onto the std::path/std::env/tokio uses underneath them. proj-db also spells its OxiProj feature oxiproj?/epsg instead of oxiproj/epsg: the sole consumer of that feature, transform::crs_to_oxioxiproj::Crs::from_epsg, is itself #[cfg(feature = "std")], and std already activates dep:oxiproj, so the sigil-less form only force-enabled a dependency that was enabled anyway — while making proj-db a second activator of the optional oxiproj. With the ?, oxiproj has exactly one activator (std), and dropping "std" from proj-db in the future would fail loudly on the missing from_epsg rather than quietly re-linking OxiProj into a no_std build. cargo tree -p oxigeo-proj --no-default-features -e normal still lists no oxiproj, and the proj-db tree still contains oxiproj + oxiproj-db. Nothing changes for std/default builds: proj-db was already a superset of them in practice.
  • oxigeo-proj: --no-default-features --features proj4rs-compat compiles for the first time (2 errors before, so again no working consumer could exist). impl From<proj4rs::errors::Error> for Error is gated on proj4rs-compat alone — the conversion needs nothing beyond alloc — but the Error::Proj4rsError variant it constructs and the Error::from_proj4rs constructor it calls were both gated on std, and the format! it uses came only from the std prelude. The variant and the constructor are now gated any(feature = "std", feature = "proj4rs-compat") (purely additive: every configuration that had them keeps them), and error.rs imports alloc::format under proj4rs-compat. New tests/proj4rs_compat_test.rs pins the three properties — the constructor is reachable, the message survives, and the Display string stays "Proj4rs error: {0}" with thiserror/std off.
  • oxigeo-proj: --no-default-features (no_std + alloc) compiles again without a std crate of its own. It previously compiled only by accident: oxiproj was a mandatory dependency, which pulled std into the compilation, and rustc collects the inherent impls of primitive types from every crate loaded into it — including std's impl f64 { fn sin(…) … }. Making oxiproj optional (see Changed above) removed that, and 73 call sites in geodesic, datum_transform, ups_projection, geoid and operation_selection stopped resolving sin/cos/tan/asin/atan/atan2/sqrt/powf/powi/ln_1p/floor/rem_euclid, none of which core provides. A new internal math module supplies them through the pure-Rust libm crate (a new, non-optional dependency — Cargo cannot express "enable when feature std is off"; libm is no_std and dependency-free, and its unused code is dropped by the linker in std builds) via a FloatExt trait whose signatures mirror the inherent methods exactly, so no call site changed and std builds still use the inherent methods. Unit tests cross-check every shim against the inherent method over a sweep of arguments; agreement is to ~1 ulp, not bit-exact, since Rust libm and the platform libm are different implementations (powi in particular is a pow call rather than LLVM's repeated squaring). Two caveats worth knowing: only the library build of --no-default-features exercises the shim — --all-targets (clippy/tests) puts the dev-dependencies and therefore std back into the compilation, so cargo check -p oxigeo-proj --no-default-features is the command that guards it; and a real bare-metal target (--target thumbv7em-none-eabihf) still fails to build, because the workspace-level byteorder = "1" keeps its default std feature — outside this crate to fix.
  • oxigeo-proj: enabling proj-db no longer changes the result of a coordinate transformation. transform::crs_to_oxi resolved a CrsSource::Epsg through oxiproj::Crs::from_epsg (oxiproj's bundled authority database) under that feature and through this crate's own PROJ-verified registry string otherwise, so the same CRS pair produced two different answers depending on the feature set. Because Crs::from_epsg itself goes through lookup_epsg, that branch could only ever fire for codes the embedded registry already carried: it added no coverage, only a second, divergent definition. Two failure modes followed. Asymmetric pairs: a PROJ-string CRS transformed against an EPSG-sourced one combined a datum-bearing definition (+towgs84 from the registry string) with a datum-less authority one, and the pipeline applied a one-sided datum shift — transforming from a code's own geodetic base to the code came out 87 m off for EPSG:2039, 226 m for EPSG:2056 and 4.8e5 m for EPSG:2314, where PROJ 9.7.0 returns the projection alone (it composes both sides' datum transformations for such a mixed pair). Divergent definitions: oxiproj 0.1.4's authority definitions disagree with PROJ even when both sides are EPSG-sourced — EPSG:2314/EPSG:24382 state the ellipsoid's semi-major axis in the CRS's own linear unit (+a=20926348, Clarke's feet) while still saying +units=m, EPSG:6933 emits +lat_1 instead of +lat_ts (a 2.5e6 m error), and EPSG:2062/EPSG:5469/EPSG:24382 fail to build a transformer at all. Under --features proj-db this failed all four transform tests of epsg_verified_registry_extended_test — 560 projection mismatches in each direction, 77 end-to-end mismatches in each direction, 3 transformer-construction failures — against fixtures that pass under default features. Every CRS is now resolved through Crs::to_proj_string()oxiproj::Crs::from_proj in all configurations; oxiproj::Crs::from_epsg is kept only as a fallback for an EPSG code the embedded registry does not carry (reachable via Deserialize), so proj-db stays strictly additive — it widens coverage without moving a number the default build already produces. Default-feature behaviour is unchanged, and the proj-db run of that test binary also got ~10x faster. New unconditional regression tests in tests/transform_test.rs pin the EPSG:2039 / EPSG:2056 base↔code pivots to their PROJ values in both directions, plus the fallback's presence (proj-db) and absence (default). The upstream oxiproj defects are reported separately.
  • oxigeo-proj: corrected the linear unit of the NAD83 State Plane EPSG registry entries (legacy zones 2222-2289/2195-2204/32164-32166 plus the NAD83(2011) zones 6355-6419) and assorted other regional CRSs whose native unit is US survey feet or international feet. The registered PROJ strings declared +units=m for zones EPSG itself defines in feet — e.g. EPSG:2222 "NAD83 / Arizona East" — so every coordinate run through oxigeo-proj's embedded registry for these codes was off by the metre/foot conversion factor (~3.28× for us-ft); false-easting/northing and standard-parallel/central-meridian constants are now the exact EPSG values (x_0=2000000.0001016 us-ft, the precise definition for the affected zones, rather than the previous x_0=2000000 m) with tightened decimal precision throughout. Separately, the reported EPSG unit name was hardcoded to "metre" at every projected-CRS registration site regardless of which PROJ string it went with — silently mislabelling 79 US-survey-foot/international-foot CRSs (the whole State Plane (ft)/(ftUS) family) plus two entries expressed via +to_meter= (Indian yard, previously reported as "metre"; Clarke's foot, previously "link"). A new epsg_unit_for helper now derives the reported unit from each entry's own +units=/+to_meter= token instead of a hardcoded default. tests/epsg_verified_registry_extended_test.rs gained PROJ-verified fixtures over the corrected zones.
  • oxigeo-proj: Transformer::transform_batch's SIMD fast path (Transverse Mercator/UTM, Mercator and Lambert Conformal Conic forward projection) no longer silently mis-projects. Previously the fast path activated whenever the target CRS's +proj= matched a supported kernel, without examining the source CRS or checking whether the kernel could faithfully reproduce the scalar OxiProj pipeline; a new fast_path_applicable gate now declines — falling back to the scalar per-point path — whenever either CRS has a non-Greenwich prime meridian, a non-ENU axis order, a real (non-@null) +nadgrids, or a named datum other than a null-shift one, or whenever parse_ellipsoid cannot recognise either CRS's ellipsoid. Three concrete bugs this closes: (1) an unrecognised +ellps (e.g. +ellps=clrk66) was silently projected on WGS-84 — measured up to 2.1e5 m off; (2) the generic Transverse Mercator kernel ignored +lat_0, so any CRS whose origin isn't the equator — e.g. the Japan Plane Rectangular CS, EPSG:6669-6687/2443-2461, +lat_0=26..44 — came out offset by the meridional arc to that latitude (≈3,985,144 m for +lat_0=36); (3) the Mercator kernel ignored +x_0/+y_0 (false easting/northing) entirely and always used k=1 even when +lat_ts should derive the scale factor, and the shared output step now converts through the target CRS's actual linear unit instead of assuming metres. The dispatch logic also moved out of transform/mod.rs into a new internal transform::simd_dispatch module; no public API changed. New tests/epsg_verified_registry_test.rs and tests/simd_batch_params_test.rs pin the fast path against both the scalar path and PROJ-verified fixtures for all four kernel families.
  • oxigeo-proj: the embedded EPSG registry's JGD2011 Japan entries are corrected. Japan Plane Rectangular CS zones I–X (EPSG:66696678) were misregistered as "JGD2011 / UTM zone 51N–60N" — a whole-family misassignment that placed Plane Rectangular data ~4,000 km east into the Pacific — and zones XI–XIX (EPSG:66796687) were absent entirely. All nineteen zones are now registered from a verified per-zone table with each zone's true lat_0/lon_0 origin, and the JGD2011 UTM zones 51N–55N live at their real codes EPSG:66886692 — the codes the Plane Rectangular family used to squat on.
  • oxigeo-gpkg: table B-tree cells whose payload spills onto SQLite overflow pages are read correctly (issue #17). Two defects combined: the local (on-page) payload size was computed as min(P, U − 35), but SQLite stores only K (or M) bytes locally when a cell overflows — 489 bytes at a 4096-byte page size, not 4061 — and the overflow-page chain was never followed at all, so a sqlite_master row wider than one page (the reporter's ~5000-character QGIS layer name) failed GeoPackage::from_bytes + load_contents() with "overflow cell needs 4061 bytes inline … but only 3209 available". The reader now computes SQLite's local-payload split against the true usable page size (page_size − reserved_bytes, see Changed) and reassembles the full payload across the chain. Regression tests in tests/issue_17_overflow_pages.rs.
  • oxigeo-vrt: SrcRect/DstRect windows parse the numeric formats GDAL actually writes (issue #18). gdalbuildvrt/gdalwarp -of VRT keep source and destination windows as doubles and round them only at rasterisation time, so real-world VRTs carry sub-pixel values such as xOff="9783.50000000003" — and parsing those attributes with str::parse::<u64> rejected every GDAL-produced mosaic with "Invalid u64: invalid digit found in string". The attributes are now parsed as f64 and rounded where GDAL rounds. (<WarpMemoryLimit>6.71089e+07</WarpMemoryLimit> was also suspected; it has parsed as f64 since 0.2.3, and a scientific-notation test now pins that so the two offenders cannot be conflated.) Regression tests in tests/issue_18_gdal_numeric_formats.rs.
  • oxigeo-vrt: mosaic compositing of overlapping ComplexSources honours each source's <NODATA> (issue #19). GDAL applies sources in document order and skips any source pixel equal to that source's nodata value — the pixel neither overwrites what is already there nor claims coverage, so a later overlapping source can still supply valid data. Previously the first source to cover a pixel won even when all it had there was nodata, punching holes along the overlap bands of every gdalbuildvrt mosaic. The comparison is made on the decoded sample value for the band's data type, not on raw bytes, so float nodata (including the NaN convention) compares correctly.

Full Changelog: v0.2.3...v0.2.4