[0.2.4] - 2026-08-18
Added
oxigeo-proj: added the unambiguous type aliasesSphericalTransverseMercator(=TransverseMercator) andEllipsoidalTransverseMercator(=GaussKruger), re-exported at the crate root, so call sites can state which Earth model they mean —TransverseMercatoris 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 kernelprojections::tmerc_forward/projections::tmerc_inversefromprojections(previously reachable only asprojections::cylindrical::tmerc_*).oxigeo-proj:transformnow re-exportsSphericalTransverseMercatorandEllipsoidalTransverseMercatoralongsideCassineSoldner/GaussKruger/TransverseMercator, souse oxigeo_proj::transform::*surfaces the two aliases instead of forcing the longertransform::cylindrical::path. Samestdgate 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 internalFile/Bytessource that implementsChunkReader, 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 toGeoParquetReader's public shape.oxigeo-geoparquet:GeoParquetReader::read_geometries_optional(row_group)andGeoParquetBatchReader::extract_geometries_optional(batch)returnVec<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 coretiff::is_mask_markers(new_subfile_type, photometric)and the marker constantstiff::SUBFILE_TYPE_TRANSPARENCY_MASK/tiff::PHOTOMETRIC_TRANSPARENCY_MASK.oxigeo-geotiff:CogReader::ifd_count(),CogReader::level_ifd(level)andCogReader::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 blockread_tileproduces at that level — the level's ownTileWidth/TileLength, orImageWidth × RowsPerStripnarrowed 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 likescan_table_by_namebut applies SQLite's REAL type affinity to the result: SQLite stores a lossless40.0in aREAL/DOUBLE/FLOAT-declared column as the integer40, so an untyped scan surfaces it as anInteger— the typed variant restores every such value to the equivalentFloat(viarestore_real_affinity, driven by the declared column types), so40and40.0read back identically, matching what every affinity-aware SQLite consumer sees.scan_table_by_nameitself is unchanged and still returns raw storage classes.
Changed
- Renamed the workspace
quick-xmldependency (Cargo.toml) to theoxixml-quickxml-compatpackage (drop-in quick-xml 0.41 compatible shim), keeping the local dependency namequick-xmlso every consuming crate (oxigeo-drivers-advanced, oxigeo-vrt, oxigeo-services, oxigeo-server, oxigeo-metadata, oxigeo-qc) required no source changes. deny.toml: added aquick-xmlentry to[bans].deny, scoped withwrappers = ["inferno"]for the one remaining transitive path (inferno -> pprof -> oxigeo-algorithms's dev-onlypprofdependency); the direct-consumer graph is clear (cargo tree -i quick-xml -e normal --workspaceis empty).oxigeo-gpkg: gated GeoJSON conversion (vector::geojson_convert, and itsoxigeo-geojson-stream/serde_jsondependencies) behind a newgeojson-convertfeature, kept indefaultso no existing build breaks;cargo build --no-default-features(e.g. for wasm) no longer pulls in theregexfamily viaoxigeo-geojson-stream. Consumers that already buildoxigeo-gpkgwithdefault-features = falsewill need to addfeatures = ["geojson-convert"]to keep usingvector::geojson_convert.oxigeo-proj: theoxiprojdependency is nowoptionaland pulled in by thestdfeature instead of being unconditional. Every OxiProj call site already lived in astd-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 normalnow lists onlybyteorder,serdeandthiserror.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 forstdexplicitly) need no change. Migration: two trait impls —impl From<oxiproj::TransformError> for Errorandimpl From<oxiproj::ProjError> for Error— are now#[cfg(feature = "std")]and therefore absent from--no-default-featuresbuilds; they could not have been used there anyway, since theoxiprojtypes they convert from were not linked. NoErrorvariant changed: all of them carryString, not OxiProj types.--no-default-features --features proj-dbremains unsupported (it was already failing to compile before this change, for unrelatedallocprelude reasons inepsg::proj_db) — superseded later in this same release:proj-dbnow impliesstdand compiles, see Fixed below.- Dependency bumps:
oxiproj0.1.5 — the OxiProj authority-path correctness release, which fixes upstream the divergent EPSG authority definitions documented in theproj-dbfeature-invariance entry under Fixed (unit-converted ellipsoid axes, method-aware+lat_tsmapping, LCC 1SP, WGS 84-hub datum composition, prime-meridian datum chains, Molodensky-Badekas operations, PROJ's ballpark/fallback selection policy, and grid direction underPROJ_DATA) — plus routine COOLJAPAN ecosystem bumps (oxiarc,oxicode,oxih5,oxionnx,oxisql,oxistore,oxitls; thequick-xml→oxixml-quickxml-compatmigration has its own entry above). oxigeo-gpkg:SqliteHeadergained the public fieldreserved_bytes: u8(byte 20 of the SQLite database header — bytes reserved at the end of every page) and ausable_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: constructingSqliteHeaderwith a struct literal outside the crate now requires the extra field; code that obtains headers throughSqliteReaderis unaffected.oxigeo-wasm:WasmCogViewer,AdvancedCogViewerandBatchTileLoaderhold their cached parsed reader (see the reader-reuse fix under Fixed) inRc/RefCelland therefore no longer implementSend/Sync. Onwasm32-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 aSend/Syncbound would notice, and none exists in the workspace.
Fixed
oxigeo-geoparquet:GeoParquetBatchReader::extract_geometriesnow dispatches on the geometry column's declared encoding instead of downcasting toBinaryArrayunconditionally — a GeoArrow-native file read throughread_all()/next_batch()previously failed with atype_mismatcherror rather than decoding.oxigeo-geoparquet: null geometries no longer silently desynchronise geometries from their property rows — the newread_geometries_optional/extract_geometries_optionalvariants keep each null as aNoneat its original index (the existing null-dropping methods are unchanged).oxigeo-wasm: GDAL internal-mask IFDs (NewSubfileTypebit 2, orPhotometricInterpretation == 4) are no longer counted as overview levels by the browser COG reader — they share the IFD chain with the overviews, sooverviewCountwas 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 itslevelargument on the URL path; it previously called a level-0 shortcut, so every overview request silently re-read full-resolution tiles.oxigeo-wasm:WasmCogViewerandAdvancedCogViewerparse 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 — forAdvancedCogViewerthat 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 normalisesModelPixelScaleTag(33550) Y to its magnitude, soWasmCogViewer.pixelScaleY(), thepixelScaleYkey of the metadata JSON, and the Rustpixel_scale_yfield ofCogMetadata/IfdMetadataare 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 theopenBytespath 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:CogReaderno longer treats GDAL internal masks as pyramid levels. A mask (NewSubfileTypebit 2, orPhotometricInterpretation == 4) shares the IFD chain with the overviews, sooverview_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, andGeoTiffReader::level_size(2)the mask's dimensions. Levels are now mapped onto non-mask IFDs and every level-indexed path — the block-offset cache, thetile_byte_rangefallback,band_read::LevelGeometry(window/band reads) andGeoTiffReader::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 whoseImageInfofailed to parse was already skipped when counting overviews but not when indexing tile offsets. Behaviour change: on a masked COGCogReader::overview_count(),GeoTiffReader::level_size,cog::get_cog_info'soverview_countand everylevelargument 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, andCogReader::ifd_count()/level_ifd_index()expose the mapping.oxigeo-wasm:AdvancedCogViewerandWasmCogViewernow report the sameoverviewCountfor the same file.AdvancedCogViewer.open()derived it fromTiffFile::image_count(), which counts every IFD including GDAL internal masks, while its ownreadTileindexedCogReader's mask-free levels andWasmCogViewerskipped 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 theCogReaderit reads through.open()also parses the file once instead of twice (it built a wholeTiffFilefor metadata and then re-opened aCogReaderon the first tile read); the reader it parses is handed straight to the tile path.oxigeo-wasm:readTileAsImageData,readTileWithContrast,computeStatsandcomputeHistogramsize their RGBA buffer from the requested level's tile geometry instead of the full-resolutiontileWidth/tileHeightcaptured atopen(). Now that tile reads honour theirlevelargument, a COG whose overviews declare a differentTileWidth/TileLength(gdaladdois free to choose one) produced anImageDataat 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_sizeforopenBytes/AdvancedCogViewer(which narrows for the short final strip of a striped level), the URL reader's own per-level record forWasmCogViewer— so buffer and bytes always agree.oxigeo-wasm:AdvancedCogViewer.open()works in a browser for the first time. The viewer parses the COG withoxigeo_geotiff::CogReader, which reads through the synchronousDataSourcetrait, but the only data source it had wasFetchBackend, whoseread_rangeis hard-wired toNotSupported("Synchronous read in WASM - use async methods")— WASM cannot block onfetch()— and which holds no bytes of its own. Everyopen()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-privatebuffered_sourcemodule inverts the loop instead of making the parser async:BufferedRangeSourceimplementsDataSourceover a cache of already-downloaded ranges and records the ranges it cannot serve, andpull_until_readyre-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 oneHEADplus one range request and a tile read costs at most one more (none at all when its block is already buffered); a server that ignoresRangeand answers200with 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::openreads overviewImageInfos, the GeoKey directory and the per-level block index best-effort and swallows the failure, so a driver that retried only onErrwould have returned a reader that silently dropped an overview or lost the file'sepsgCode. 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.AdvancedCogViewernow 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. TheopenBytes(in-memory) path is untouched. Native tests drive the whole loop over synthetic TIFF bytes with an in-memory transport; only theweb_sys-backed implementation of the fetch seam — a thin translation of onefetch()response, with the response decoding split out and tested — is browser-only.oxigeo-wasm: thepyramidblock ofAdvancedCogViewer.getMetadata()no longer contradicts theoverviewCountprinted beside it. It was built from aTilePyramidsynthesised 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 reportednumLevels: 5next tooverviewCount: 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 chainoverviewCountcomes from — with each level's own dimensions and block size read back throughCogReader::level_ifd. Value changes (keys are unchanged):numLevelsis now alwaysoverviewCount + 1;tilesPerLevelhas one[tilesX, tilesY]entry per real level, computed from that level's ownImageWidth/ImageLengthandTileWidth/TileLength(for a striped level, image width byRowsPerStrip) instead of from repeatedly halved level-0 dimensions;totalTilesis the sum over those real levels, and remains a count of spatial blocks — a planar (PlanarConfiguration = 2) file storesSamplesPerPixeltimes as many. One key is added:pyramid.levels, an array of{width, height, tileWidth, tileHeight, tilesX, tilesY}in level order.TilePyramiditself 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-dbcompiles for the first time (it produced 56 errors before, so no build could ever have depended on its previous behaviour).proj-dbnow impliesstd: the feature is not expressible on ano_std+allocbuild, becauseepsg::proj_dbopens a file-system database (std::path::{Path, PathBuf},std::env::varforPROJ_DATA/PROJ_LIB) and drivesoxisql-sqlite-compat's async engine through itsblockingAPI on acurrent_threadtokio runtime. The alternative — sprinklingallocprelude imports overepsg/proj_db.rs— would only have moved the 56 errors onto thestd::path/std::env/tokio uses underneath them.proj-dbalso spells its OxiProj featureoxiproj?/epsginstead ofoxiproj/epsg: the sole consumer of that feature,transform::crs_to_oxi→oxiproj::Crs::from_epsg, is itself#[cfg(feature = "std")], andstdalready activatesdep:oxiproj, so the sigil-less form only force-enabled a dependency that was enabled anyway — while makingproj-dba second activator of the optionaloxiproj. With the?,oxiprojhas exactly one activator (std), and dropping"std"fromproj-dbin the future would fail loudly on the missingfrom_epsgrather than quietly re-linking OxiProj into ano_stdbuild.cargo tree -p oxigeo-proj --no-default-features -e normalstill lists nooxiproj, and theproj-dbtree still containsoxiproj+oxiproj-db. Nothing changes forstd/default builds:proj-dbwas already a superset of them in practice.oxigeo-proj:--no-default-features --features proj4rs-compatcompiles for the first time (2 errors before, so again no working consumer could exist).impl From<proj4rs::errors::Error> for Erroris gated onproj4rs-compatalone — the conversion needs nothing beyondalloc— but theError::Proj4rsErrorvariant it constructs and theError::from_proj4rsconstructor it calls were both gated onstd, and theformat!it uses came only from thestdprelude. The variant and the constructor are now gatedany(feature = "std", feature = "proj4rs-compat")(purely additive: every configuration that had them keeps them), anderror.rsimportsalloc::formatunderproj4rs-compat. Newtests/proj4rs_compat_test.rspins the three properties — the constructor is reachable, the message survives, and theDisplaystring stays"Proj4rs error: {0}"withthiserror/stdoff.oxigeo-proj:--no-default-features(no_std+alloc) compiles again without astdcrate of its own. It previously compiled only by accident:oxiprojwas a mandatory dependency, which pulledstdinto the compilation, and rustc collects the inherent impls of primitive types from every crate loaded into it — includingstd'simpl f64 { fn sin(…) … }. Makingoxiprojoptional (see Changed above) removed that, and 73 call sites ingeodesic,datum_transform,ups_projection,geoidandoperation_selectionstopped resolvingsin/cos/tan/asin/atan/atan2/sqrt/powf/powi/ln_1p/floor/rem_euclid, none of whichcoreprovides. A new internalmathmodule supplies them through the pure-Rustlibmcrate (a new, non-optional dependency — Cargo cannot express "enable when featurestdis off";libmisno_stdand dependency-free, and its unused code is dropped by the linker instdbuilds) via aFloatExttrait whose signatures mirror the inherent methods exactly, so no call site changed andstdbuilds 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 Rustlibmand the platform libm are different implementations (powiin particular is apowcall rather than LLVM's repeated squaring). Two caveats worth knowing: only the library build of--no-default-featuresexercises the shim —--all-targets(clippy/tests) puts the dev-dependencies and thereforestdback into the compilation, socargo check -p oxigeo-proj --no-default-featuresis the command that guards it; and a real bare-metal target (--target thumbv7em-none-eabihf) still fails to build, because the workspace-levelbyteorder = "1"keeps its defaultstdfeature — outside this crate to fix.oxigeo-proj: enablingproj-dbno longer changes the result of a coordinate transformation.transform::crs_to_oxiresolved aCrsSource::Epsgthroughoxiproj::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. BecauseCrs::from_epsgitself goes throughlookup_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 (+towgs84from 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 forEPSG:2039, 226 m forEPSG:2056and 4.8e5 m forEPSG: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:24382state the ellipsoid's semi-major axis in the CRS's own linear unit (+a=20926348, Clarke's feet) while still saying+units=m,EPSG:6933emits+lat_1instead of+lat_ts(a 2.5e6 m error), andEPSG:2062/EPSG:5469/EPSG:24382fail to build a transformer at all. Under--features proj-dbthis failed all four transform tests ofepsg_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 throughCrs::to_proj_string()→oxiproj::Crs::from_projin all configurations;oxiproj::Crs::from_epsgis kept only as a fallback for an EPSG code the embedded registry does not carry (reachable viaDeserialize), soproj-dbstays strictly additive — it widens coverage without moving a number the default build already produces. Default-feature behaviour is unchanged, and theproj-dbrun of that test binary also got ~10x faster. New unconditional regression tests intests/transform_test.rspin theEPSG:2039/EPSG:2056base↔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=mfor zones EPSG itself defines in feet — e.g.EPSG:2222"NAD83 / Arizona East" — so every coordinate run throughoxigeo-proj's embedded registry for these codes was off by the metre/foot conversion factor (~3.28× forus-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 previousx_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 newepsg_unit_forhelper 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.rsgained 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 newfast_path_applicablegate 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 wheneverparse_ellipsoidcannot 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 usedk=1even when+lat_tsshould 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 oftransform/mod.rsinto a new internaltransform::simd_dispatchmodule; no public API changed. Newtests/epsg_verified_registry_test.rsandtests/simd_batch_params_test.rspin 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:6669–6678) 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:6679–6687) were absent entirely. All nineteen zones are now registered from a verified per-zone table with each zone's truelat_0/lon_0origin, and the JGD2011 UTM zones 51N–55N live at their real codesEPSG:6688–6692— 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 asmin(P, U − 35), but SQLite stores onlyK(orM) 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 asqlite_masterrow wider than one page (the reporter's ~5000-character QGIS layer name) failedGeoPackage::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 intests/issue_17_overflow_pages.rs.oxigeo-vrt:SrcRect/DstRectwindows parse the numeric formats GDAL actually writes (issue #18).gdalbuildvrt/gdalwarp -of VRTkeep source and destination windows as doubles and round them only at rasterisation time, so real-world VRTs carry sub-pixel values such asxOff="9783.50000000003"— and parsing those attributes withstr::parse::<u64>rejected every GDAL-produced mosaic with "Invalid u64: invalid digit found in string". The attributes are now parsed asf64and rounded where GDAL rounds. (<WarpMemoryLimit>6.71089e+07</WarpMemoryLimit>was also suspected; it has parsed asf64since 0.2.3, and a scientific-notation test now pins that so the two offenders cannot be conflated.) Regression tests intests/issue_18_gdal_numeric_formats.rs.oxigeo-vrt: mosaic compositing of overlappingComplexSources 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 everygdalbuildvrtmosaic. 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