From 575661500d18c1cf27f59837999242ff84872bd5 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:28:06 +0800 Subject: [PATCH 01/13] feat(sedona-gdal): add dataset and vector/raster wrappers --- c/sedona-gdal/src/raster.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/c/sedona-gdal/src/raster.rs b/c/sedona-gdal/src/raster.rs index 839674298..8124335c4 100644 --- a/c/sedona-gdal/src/raster.rs +++ b/c/sedona-gdal/src/raster.rs @@ -20,3 +20,5 @@ pub mod rasterband; pub mod rasterize; pub mod rasterize_affine; pub mod types; + +pub use rasterband::{actual_block_size, RasterBand}; From ef70c2341f412d425f674f59b35de15a2e670383 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:43:28 +0800 Subject: [PATCH 02/13] refactor(sedona-gdal): prefer imported core types --- c/sedona-gdal/src/vector/feature.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/c/sedona-gdal/src/vector/feature.rs b/c/sedona-gdal/src/vector/feature.rs index a80648b17..edda26e7a 100644 --- a/c/sedona-gdal/src/vector/feature.rs +++ b/c/sedona-gdal/src/vector/feature.rs @@ -162,7 +162,11 @@ impl<'a> BorrowedGeometry<'a> { Ok(buf) } +<<<<<<< HEAD /// Fetch the 2D envelope of this geometry. +======= + /// Get the bounding envelope. +>>>>>>> 46f573c4 (refactor(sedona-gdal): prefer imported core types) pub fn envelope(&self) -> Envelope { let mut env = OGREnvelope { MinX: 0.0, @@ -201,7 +205,14 @@ impl FieldDefn { let c_field_defn = unsafe { call_gdal_api!(api, OGR_Fld_Create, c_name.as_ptr(), field_type) }; if c_field_defn.is_null() { +<<<<<<< HEAD return Err(api.last_null_pointer_err("OGR_Fld_Create")); +======= + return Err(GdalError::NullPointer { + method_name: "OGR_Fld_Create", + msg: format!("failed to create field definition '{name}'"), + }); +>>>>>>> 46f573c4 (refactor(sedona-gdal): prefer imported core types) } Ok(Self { api, c_field_defn }) } From 8809a0ae8e1f31088ce614487b8cabd995f2d793 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Fri, 13 Mar 2026 14:32:55 +0800 Subject: [PATCH 03/13] refactor(sedona-gdal): remove wrapper re-export aliases --- c/sedona-gdal/src/raster.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/c/sedona-gdal/src/raster.rs b/c/sedona-gdal/src/raster.rs index 8124335c4..839674298 100644 --- a/c/sedona-gdal/src/raster.rs +++ b/c/sedona-gdal/src/raster.rs @@ -20,5 +20,3 @@ pub mod rasterband; pub mod rasterize; pub mod rasterize_affine; pub mod types; - -pub use rasterband::{actual_block_size, RasterBand}; From eb8c928098aa19a05727234f3c5bc7226c01f45e Mon Sep 17 00:00:00 2001 From: Kontinuation Date: Thu, 19 Mar 2026 00:05:56 +0800 Subject: [PATCH 04/13] fix(sedona-gdal): use last null pointer helper --- c/sedona-gdal/src/vector/feature.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/c/sedona-gdal/src/vector/feature.rs b/c/sedona-gdal/src/vector/feature.rs index edda26e7a..4a2eaed78 100644 --- a/c/sedona-gdal/src/vector/feature.rs +++ b/c/sedona-gdal/src/vector/feature.rs @@ -205,14 +205,7 @@ impl FieldDefn { let c_field_defn = unsafe { call_gdal_api!(api, OGR_Fld_Create, c_name.as_ptr(), field_type) }; if c_field_defn.is_null() { -<<<<<<< HEAD return Err(api.last_null_pointer_err("OGR_Fld_Create")); -======= - return Err(GdalError::NullPointer { - method_name: "OGR_Fld_Create", - msg: format!("failed to create field definition '{name}'"), - }); ->>>>>>> 46f573c4 (refactor(sedona-gdal): prefer imported core types) } Ok(Self { api, c_field_defn }) } From 00f46e92fc5cb78a1785a8769c97ab15ffb2e576 Mon Sep 17 00:00:00 2001 From: Kontinuation Date: Thu, 19 Mar 2026 09:27:53 +0800 Subject: [PATCH 05/13] test(sedona-gdal): add layer iteration coverage --- c/sedona-gdal/src/dataset.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs index 36832d866..c2a1d2fd4 100644 --- a/c/sedona-gdal/src/dataset.rs +++ b/c/sedona-gdal/src/dataset.rs @@ -38,10 +38,17 @@ pub struct Dataset { c_dataset: GDALDatasetH, } +<<<<<<< HEAD // SAFETY: `Dataset` has unique ownership of its GDAL dataset handle and only moves // that ownership across threads. The handle is closed exactly once on drop, and this // wrapper does not provide shared concurrent access, so `Send` is sound while `Sync` // remains intentionally unimplemented. +======= +// SAFETY: `Dataset` carries an opaque GDAL dataset handle plus an ownership flag. +// Moving the wrapper across threads only transfers ownership of that handle; it does +// not permit concurrent shared access. The handle is closed at most once on drop when +// `owned` is true, so `Send` is sound while `Sync` remains intentionally unimplemented. +>>>>>>> b522280f (test(sedona-gdal): add layer iteration coverage) unsafe impl Send for Dataset {} impl Drop for Dataset { From fb80d5654835a87719c31aa88adc549c8ce97bf4 Mon Sep 17 00:00:00 2001 From: Kontinuation Date: Thu, 19 Mar 2026 22:26:51 +0800 Subject: [PATCH 06/13] docs(sedona-gdal): tighten wrapper API comments --- c/sedona-gdal/src/vector/feature.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/c/sedona-gdal/src/vector/feature.rs b/c/sedona-gdal/src/vector/feature.rs index 4a2eaed78..a80648b17 100644 --- a/c/sedona-gdal/src/vector/feature.rs +++ b/c/sedona-gdal/src/vector/feature.rs @@ -162,11 +162,7 @@ impl<'a> BorrowedGeometry<'a> { Ok(buf) } -<<<<<<< HEAD /// Fetch the 2D envelope of this geometry. -======= - /// Get the bounding envelope. ->>>>>>> 46f573c4 (refactor(sedona-gdal): prefer imported core types) pub fn envelope(&self) -> Envelope { let mut env = OGREnvelope { MinX: 0.0, From bc23f6f9c3d9bcd17a831ded7354eac8e7752af3 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:30:34 +0800 Subject: [PATCH 07/13] feat(sedona-gdal): add convenience facade and mem builder --- c/sedona-gdal/src/gdal.rs | 250 +++++++++++++++++ c/sedona-gdal/src/gdal_api.rs | 1 + c/sedona-gdal/src/global.rs | 15 ++ c/sedona-gdal/src/lib.rs | 2 + c/sedona-gdal/src/mem.rs | 486 ++++++++++++++++++++++++++++++++++ 5 files changed, 754 insertions(+) create mode 100644 c/sedona-gdal/src/gdal.rs create mode 100644 c/sedona-gdal/src/mem.rs diff --git a/c/sedona-gdal/src/gdal.rs b/c/sedona-gdal/src/gdal.rs new file mode 100644 index 000000000..246d647d4 --- /dev/null +++ b/c/sedona-gdal/src/gdal.rs @@ -0,0 +1,250 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! High-level convenience wrapper around [`GdalApi`]. +//! +//! [`Gdal`] bundles a `&'static GdalApi` reference and exposes ergonomic +//! methods that delegate to the lower-level constructors and free functions +//! scattered across the crate, eliminating the need to pass `api` explicitly +//! at every call site. + +use crate::config; +use crate::dataset::Dataset; +use crate::driver::{Driver, DriverManager}; +use crate::errors::Result; +use crate::gdal_api::GdalApi; +use crate::gdal_dyn_bindgen::{GDALOpenFlags, OGRFieldType}; +use crate::raster::polygonize::{polygonize, PolygonizeOptions}; +use crate::raster::rasterize::{rasterize, RasterizeOptions}; +use crate::raster::rasterize_affine::rasterize_affine; +use crate::raster::types::DatasetOptions; +use crate::raster::types::GdalDataType; +use crate::raster::RasterBand; +use crate::spatial_ref::SpatialRef; +use crate::vector::feature::FieldDefn; +use crate::vector::geometry::Geometry; +use crate::vector::Layer; +use crate::vrt::VrtDataset; +use crate::vsi; + +/// High-level convenience wrapper around [`GdalApi`]. +/// +/// Stores a `&'static GdalApi` reference and provides ergonomic methods that +/// delegate to the various constructors and free functions in the crate. +pub struct Gdal { + api: &'static GdalApi, +} + +impl Gdal { + /// Create a new `Gdal` instance wrapping the given API reference. + pub(crate) fn new(api: &'static GdalApi) -> Self { + Self { api } + } + + // -- Info ---------------------------------------------------------------- + + /// Return the name of the loaded GDAL library. + pub fn name(&self) -> &str { + self.api.name() + } + + /// Query GDAL version information. + /// + /// `request` is one of the standard `GDALVersionInfo` keys: + /// - `"RELEASE_NAME"` — e.g. `"3.8.4"` + /// - `"VERSION_NUM"` — e.g. `"3080400"` + /// - `"BUILD_INFO"` — multi-line build details + pub fn version_info(&self, request: &str) -> String { + self.api.version_info(request) + } + + // -- Config -------------------------------------------------------------- + + /// Set a GDAL library configuration option with **thread-local** scope. + pub fn set_thread_local_config_option(&self, key: &str, value: &str) -> Result<()> { + config::set_thread_local_config_option(self.api, key, value) + } + + // -- Driver -------------------------------------------------------------- + + /// Look up a GDAL driver by its short name (e.g. `"GTiff"`, `"MEM"`). + pub fn get_driver_by_name(&self, name: &str) -> Result { + DriverManager::get_driver_by_name(self.api, name) + } + + // -- Dataset ------------------------------------------------------------- + + /// Open a dataset with extended options. + pub fn open_ex( + &self, + path: &str, + open_flags: GDALOpenFlags, + allowed_drivers: Option<&[&str]>, + open_options: Option<&[&str]>, + sibling_files: Option<&[&str]>, + ) -> Result { + Dataset::open_ex( + self.api, + path, + open_flags, + allowed_drivers, + open_options, + sibling_files, + ) + } + + /// Open a dataset using a [`DatasetOptions`] struct (georust-compatible convenience). + pub fn open_ex_with_options(&self, path: &str, options: DatasetOptions<'_>) -> Result { + Dataset::open_ex_with_options(self.api, path, options) + } + + // -- Spatial Reference --------------------------------------------------- + + /// Create a new [`SpatialRef`] from a WKT string. + pub fn spatial_ref_from_wkt(&self, wkt: &str) -> Result { + SpatialRef::from_wkt(self.api, wkt) + } + + // -- VRT ----------------------------------------------------------------- + + /// Create a new empty VRT dataset with the given dimensions. + pub fn create_vrt(&self, x_size: usize, y_size: usize) -> Result { + VrtDataset::create(self.api, x_size, y_size) + } + + // -- Geometry ------------------------------------------------------------ + + /// Create a geometry from WKB bytes. + pub fn geometry_from_wkb(&self, wkb: &[u8]) -> Result { + Geometry::from_wkb(self.api, wkb) + } + + /// Create a geometry from a WKT string. + pub fn geometry_from_wkt(&self, wkt: &str) -> Result { + Geometry::from_wkt(self.api, wkt) + } + + // -- Vector -------------------------------------------------------------- + + /// Create a new OGR field definition. + pub fn create_field_defn(&self, name: &str, field_type: OGRFieldType) -> Result { + FieldDefn::new(self.api, name, field_type) + } + + // -- VSI (Virtual File System) ------------------------------------------- + + /// Create a new VSI in-memory file from a given buffer. + pub fn create_mem_file(&self, file_name: &str, data: Vec) -> Result<()> { + vsi::create_mem_file(self.api, file_name, data) + } + + /// Unlink (delete) a VSI in-memory file. + pub fn unlink_mem_file(&self, file_name: &str) -> Result<()> { + vsi::unlink_mem_file(self.api, file_name) + } + + /// Copy the bytes of a VSI in-memory file, taking ownership and freeing the + /// GDAL memory. + pub fn get_vsi_mem_file_bytes_owned(&self, file_name: &str) -> Result> { + vsi::get_vsi_mem_file_bytes_owned(self.api, file_name) + } + + // -- Raster operations --------------------------------------------------- + + /// Create a bare in-memory (MEM) GDAL dataset via `MEMDataset::Create`. + /// + /// This bypasses GDAL's open-dataset-list mutex for better concurrency. + /// The returned dataset has `n_owned_bands` bands of type + /// `owned_bands_data_type` whose pixel data is owned by GDAL. + /// + /// For a higher-level builder that also attaches zero-copy external bands, + /// geo-transforms, projections, and nodata values, see + /// [`MemDatasetBuilder`](crate::mem::MemDatasetBuilder). + pub fn create_mem_dataset( + &self, + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Result { + crate::mem::create_mem_dataset( + self.api, + width, + height, + n_owned_bands, + owned_bands_data_type, + ) + } + + /// Rasterize geometries with an affine transformer derived from the + /// destination dataset. + pub fn rasterize_affine( + &self, + dataset: &Dataset, + bands: &[usize], + geometries: &[Geometry], + burn_values: &[f64], + all_touched: bool, + ) -> Result<()> { + rasterize_affine( + self.api, + dataset, + bands, + geometries, + burn_values, + all_touched, + ) + } + + /// Rasterize geometries onto a dataset. + pub fn rasterize( + &self, + dataset: &Dataset, + band_list: &[i32], + geometries: &[&Geometry], + burn_values: &[f64], + options: Option, + ) -> Result<()> { + rasterize( + self.api, + dataset, + band_list, + geometries, + burn_values, + options, + ) + } + + /// Polygonize a raster band into a vector layer. + pub fn polygonize( + &self, + src_band: &RasterBand<'_>, + mask_band: Option<&RasterBand<'_>>, + out_layer: &Layer<'_>, + pixel_value_field: i32, + options: &PolygonizeOptions, + ) -> Result<()> { + polygonize( + self.api, + src_band, + mask_band, + out_layer, + pixel_value_field, + options, + ) + } +} diff --git a/c/sedona-gdal/src/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs index c4baa6b19..cef854d4c 100644 --- a/c/sedona-gdal/src/gdal_api.rs +++ b/c/sedona-gdal/src/gdal_api.rs @@ -33,6 +33,7 @@ use crate::gdal_dyn_bindgen::SedonaGdalApi; /// initialization of [`GdalApi`] via [`GdalApi::try_from_shared_library`] or /// [`GdalApi::try_from_current_process`], and you cannot obtain a `&GdalApi` /// without successful initialization. +#[macro_export] macro_rules! call_gdal_api { ($api:expr, $func:ident $(, $arg:expr)*) => { if let Some(func) = $api.inner.$func { diff --git a/c/sedona-gdal/src/global.rs b/c/sedona-gdal/src/global.rs index 9365ef89a..c09d6c76e 100644 --- a/c/sedona-gdal/src/global.rs +++ b/c/sedona-gdal/src/global.rs @@ -16,6 +16,7 @@ // under the License. use crate::errors::GdalInitLibraryError; +use crate::gdal::Gdal; use crate::gdal_api::GdalApi; use std::path::PathBuf; use std::sync::{Mutex, OnceLock}; @@ -203,6 +204,20 @@ where Ok(func(api)) } +/// Execute a closure with a high-level [`Gdal`] handle backed by the +/// process-global [`GdalApi`]. +/// +/// This is the ergonomic entry-point for most GDAL operations. The global API +/// is initialized lazily on the first call; subsequent calls reuse the same +/// `&'static GdalApi` under the hood. +pub fn with_global_gdal(func: F) -> Result +where + F: FnOnce(&Gdal) -> R, +{ + let api = get_global_gdal_api()?; + Ok(func(&Gdal::new(api))) +} + /// Verify that the GDAL library meets the minimum version requirement. /// /// We use `GDALVersionInfo("VERSION_NUM")` instead of `GDALCheckVersion` because diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs index f61384515..6f8f70275 100644 --- a/c/sedona-gdal/src/lib.rs +++ b/c/sedona-gdal/src/lib.rs @@ -23,6 +23,7 @@ pub mod gdal_dyn_bindgen; pub mod errors; // --- Core API --- +pub mod gdal; pub mod gdal_api; pub mod global; @@ -32,6 +33,7 @@ pub mod cpl; pub mod dataset; pub mod driver; pub mod geo_transform; +pub mod mem; pub mod raster; pub mod spatial_ref; pub mod vector; diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs new file mode 100644 index 000000000..16ab649a8 --- /dev/null +++ b/c/sedona-gdal/src/mem.rs @@ -0,0 +1,486 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! High-level builder for creating in-memory (MEM) GDAL datasets. +//! +//! [`MemDatasetBuilder`] provides a fluent, type-safe API for constructing GDAL MEM +//! datasets with zero-copy band attachment, optional geo-transform, projection, and +//! per-band nodata values. +//! +//! # Example +//! +//! ```rust,ignore +//! use sedona_gdal::global::with_global_gdal; +//! use sedona_gdal::mem::{MemDatasetBuilder, Nodata}; +//! use sedona_gdal::GdalDataType; +//! +//! with_global_gdal(|gdal| { +//! let data: Vec = vec![0u8; 256 * 256]; +//! let dataset = unsafe { +//! MemDatasetBuilder::new(256, 256) +//! .add_band(GdalDataType::UInt8, data.as_ptr()) +//! .geo_transform([0.0, 1.0, 0.0, 0.0, 0.0, -1.0]) +//! .projection("EPSG:4326") +//! .build(gdal) +//! .unwrap() +//! }; +//! assert_eq!(dataset.raster_count(), 1); +//! }).unwrap(); +//! ``` + +use crate::dataset::Dataset; +use crate::errors::Result; +use crate::gdal::Gdal; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::CE_Failure; +use crate::raster::types::GdalDataType; + +/// Nodata value for a raster band. +/// +/// GDAL has three separate APIs for setting nodata depending on the band data type: +/// - [`f64`] for most types (UInt8 through Float64, excluding Int64/UInt64) +/// - [`i64`] for Int64 bands +/// - [`u64`] for UInt64 bands +/// +/// This enum encapsulates all three variants so callers don't need to match on +/// the band type when setting nodata. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Nodata { + F64(f64), + I64(i64), + U64(u64), +} + +/// A band specification for [`MemDatasetBuilder`]. +struct MemBand { + data_type: GdalDataType, + data_ptr: *const u8, + pixel_offset: Option, + line_offset: Option, + nodata: Option, +} + +/// A builder for constructing in-memory (MEM) GDAL datasets. +/// +/// This creates datasets using `MEMDataset::Create` (bypassing GDAL's open-dataset-list +/// mutex for better concurrency) and attaches bands via `GDALAddBand` with `DATAPOINTER` +/// options for zero-copy operation. +/// +/// # Safety +/// +/// All `add_band*` methods are `unsafe` because the caller must ensure that the +/// provided data pointers remain valid for the lifetime of the built [`Dataset`]. +pub struct MemDatasetBuilder { + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: Option, + bands: Vec, + geo_transform: Option<[f64; 6]>, + projection: Option, +} + +impl MemDatasetBuilder { + /// Create a new builder for a MEM dataset with the given dimensions. + pub fn new(width: usize, height: usize) -> Self { + Self { + width, + height, + n_owned_bands: 0, + owned_bands_data_type: None, + bands: Vec::new(), + geo_transform: None, + projection: None, + } + } + + /// Create a new builder for a MEM dataset with the given dimensions and number of owned bands. + pub fn new_with_owned_bands( + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Self { + Self { + width, + height, + n_owned_bands, + owned_bands_data_type: Some(owned_bands_data_type), + bands: Vec::new(), + geo_transform: None, + projection: None, + } + } + + /// Create a MEM dataset with owned bands. + /// + /// This is a convenience shortcut equivalent to + /// `MemDatasetBuilder::new_with_owned_bands(...).build(gdal)`. + /// + /// Unlike [`build`](Self::build), this method is safe because datasets created + /// with only owned bands do not reference any external memory. + pub fn create( + gdal: &Gdal, + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Result { + // SAFETY: `new_with_owned_bands` creates a builder with zero external bands, + // so no data pointers need to outlive the dataset. + unsafe { + Self::new_with_owned_bands(width, height, n_owned_bands, owned_bands_data_type) + .build(gdal) + } + } + + /// Add a zero-copy band from a raw data pointer. + /// + /// Uses default pixel and line offsets (contiguous, row-major layout). + /// + /// # Safety + /// + /// The caller must ensure `data_ptr` points to a valid buffer of at least + /// `height * width * data_type.byte_size()` bytes, and that the buffer + /// outlives the built [`Dataset`]. + pub unsafe fn add_band(self, data_type: GdalDataType, data_ptr: *const u8) -> Self { + self.add_band_with_options(data_type, data_ptr, None, None, None) + } + + /// Add a zero-copy band with custom offsets and optional nodata. + /// + /// # Arguments + /// * `data_type` - The GDAL data type of the band. + /// * `data_ptr` - Pointer to the band pixel data. + /// * `pixel_offset` - Byte offset between consecutive pixels. `None` defaults to + /// the byte size of `data_type`. + /// * `line_offset` - Byte offset between consecutive lines. `None` defaults to + /// `pixel_offset * width`. + /// * `nodata` - Optional nodata value for the band. + /// + /// # Safety + /// + /// The caller must ensure `data_ptr` points to a valid buffer of sufficient size + /// for the given dimensions and offsets, and that the buffer outlives the built + /// [`Dataset`]. + pub unsafe fn add_band_with_options( + mut self, + data_type: GdalDataType, + data_ptr: *const u8, + pixel_offset: Option, + line_offset: Option, + nodata: Option, + ) -> Self { + self.bands.push(MemBand { + data_type, + data_ptr, + pixel_offset, + line_offset, + nodata, + }); + self + } + + /// Set the geo-transform for the dataset. + /// + /// The array is `[origin_x, pixel_width, rotation_x, origin_y, rotation_y, pixel_height]`. + pub fn geo_transform(mut self, gt: [f64; 6]) -> Self { + self.geo_transform = Some(gt); + self + } + + /// Set the projection (CRS) for the dataset as a WKT or PROJ string. + pub fn projection(mut self, wkt: impl Into) -> Self { + self.projection = Some(wkt.into()); + self + } + + /// Build the GDAL MEM dataset. + /// + /// Creates an empty MEM dataset using [`create_mem_dataset`], then attaches + /// bands, sets the geo-transform, projection, and per-band nodata values. + /// + /// # Safety + /// + /// This method is unsafe because the built dataset references memory provided via + /// the `add_band*` methods. The caller must ensure all data pointers remain valid + /// for the lifetime of the returned [`Dataset`]. + pub unsafe fn build(self, gdal: &Gdal) -> Result { + let dataset = gdal.create_mem_dataset( + self.width, + self.height, + self.n_owned_bands, + self.owned_bands_data_type.unwrap_or(GdalDataType::UInt8), + )?; + + // Attach bands (zero-copy via DATAPOINTER). + for band_spec in &self.bands { + dataset.add_band_with_data( + band_spec.data_type, + band_spec.data_ptr, + band_spec.pixel_offset, + band_spec.line_offset, + )?; + } + + // Set geo-transform. + if let Some(gt) = &self.geo_transform { + dataset.set_geo_transform(gt)?; + } + + // Set projection/CRS. + if let Some(proj) = &self.projection { + dataset.set_projection(proj)?; + } + + // Set per-band nodata values. + for (i, band_spec) in self.bands.iter().enumerate() { + if let Some(nodata) = &band_spec.nodata { + let raster_band = dataset.rasterband(i + 1 + self.n_owned_bands)?; + match nodata { + Nodata::F64(v) => raster_band.set_no_data_value(Some(*v))?, + Nodata::I64(v) => raster_band.set_no_data_value_i64(Some(*v))?, + Nodata::U64(v) => raster_band.set_no_data_value_u64(Some(*v))?, + } + } + } + + Ok(dataset) + } +} + +/// Create a bare in-memory (MEM) GDAL dataset via `MEMDataset::Create`. +/// +/// This bypasses GDAL's open-dataset-list mutex for better concurrency. +/// The returned dataset has `n_owned_bands` bands of type +/// `owned_bands_data_type` whose pixel data is owned by GDAL. +/// +/// For a higher-level builder that also attaches zero-copy external bands, +/// geo-transforms, projections, and nodata values, see [`MemDatasetBuilder`]. +pub(crate) fn create_mem_dataset( + api: &'static GdalApi, + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, +) -> Result { + let empty_filename = c""; + let c_data_type = owned_bands_data_type.to_c(); + let handle = unsafe { + call_gdal_api!( + api, + MEMDatasetCreate, + empty_filename.as_ptr(), + width.try_into()?, + height.try_into()?, + n_owned_bands.try_into()?, + c_data_type, + std::ptr::null_mut() + ) + }; + + if handle.is_null() { + return Err(api.last_cpl_err(CE_Failure as u32)); + } + Ok(Dataset::new_owned(api, handle)) +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::global::with_global_gdal; + use crate::mem::{MemDatasetBuilder, Nodata}; + use crate::raster::types::GdalDataType; + + #[test] + fn test_mem_builder_single_band() { + with_global_gdal(|gdal| { + let data = vec![42u8; 64 * 64]; + let dataset = unsafe { + MemDatasetBuilder::new(64, 64) + .add_band(GdalDataType::UInt8, data.as_ptr()) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_size(), (64, 64)); + assert_eq!(dataset.raster_count(), 1); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_multi_band() { + with_global_gdal(|gdal| { + let band1 = vec![1u16; 32 * 32]; + let band2 = vec![2u16; 32 * 32]; + let band3 = vec![3u16; 32 * 32]; + let dataset = unsafe { + MemDatasetBuilder::new(32, 32) + .add_band(GdalDataType::UInt16, band1.as_ptr() as *const u8) + .add_band(GdalDataType::UInt16, band2.as_ptr() as *const u8) + .add_band(GdalDataType::UInt16, band3.as_ptr() as *const u8) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 3); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_with_geo_transform() { + with_global_gdal(|gdal| { + let data = vec![0f32; 10 * 10]; + let gt = [100.0, 0.5, 0.0, 200.0, 0.0, -0.5]; + let dataset = unsafe { + MemDatasetBuilder::new(10, 10) + .add_band(GdalDataType::Float32, data.as_ptr() as *const u8) + .geo_transform(gt) + .build(gdal) + .unwrap() + }; + let got = dataset.geo_transform().unwrap(); + assert_eq!(gt, got); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_with_projection() { + with_global_gdal(|gdal| { + let data = [0u8; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new(8, 8) + .add_band(GdalDataType::UInt8, data.as_ptr()) + .projection(r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#) + .build(gdal) + .unwrap() + }; + let proj = dataset.projection(); + assert!(proj.contains("WGS 84"), "Expected WGS 84 in: {proj}"); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_with_nodata() { + with_global_gdal(|gdal| { + let data = [0f64; 4 * 4]; + let dataset = unsafe { + MemDatasetBuilder::new(4, 4) + .add_band_with_options( + GdalDataType::Float64, + data.as_ptr() as *const u8, + None, + None, + Some(Nodata::F64(-9999.0)), + ) + .build(gdal) + .unwrap() + }; + let band = dataset.rasterband(1).unwrap(); + let nodata = band.no_data_value(); + assert_eq!(nodata, Some(-9999.0)); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_zero_bands() { + with_global_gdal(|gdal| { + let dataset = unsafe { MemDatasetBuilder::new(16, 16).build(gdal).unwrap() }; + assert_eq!(dataset.raster_count(), 0); + assert_eq!(dataset.raster_size(), (16, 16)); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_mixed_band_types() { + with_global_gdal(|gdal| { + let band_u8 = [0u8; 8 * 8]; + let band_f64 = vec![0f64; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new(8, 8) + .add_band(GdalDataType::UInt8, band_u8.as_ptr()) + .add_band(GdalDataType::Float64, band_f64.as_ptr() as *const u8) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + }) + .unwrap(); + } + + #[test] + pub fn test_mem_builder_with_owned_bands() { + with_global_gdal(|gdal| { + let dataset = unsafe { + MemDatasetBuilder::new_with_owned_bands(16, 16, 2, GdalDataType::UInt16) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::UInt16 + ); + assert_eq!( + dataset.rasterband(2).unwrap().band_type(), + GdalDataType::UInt16 + ); + + let dataset = MemDatasetBuilder::create(gdal, 10, 8, 1, GdalDataType::Float32).unwrap(); + assert_eq!(dataset.raster_count(), 1); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::Float32 + ); + }) + .unwrap(); + } + + #[test] + pub fn test_mem_builder_mixed_owned_and_external_bands() { + with_global_gdal(|gdal| { + let external_band = [0u8; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new_with_owned_bands(8, 8, 1, GdalDataType::Float32) + .add_band_with_options( + GdalDataType::UInt8, + external_band.as_ptr(), + None, + None, + Some(Nodata::U64(255)), + ) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::Float32 + ); + assert_eq!( + dataset.rasterband(2).unwrap().band_type(), + GdalDataType::UInt8 + ); + let nodata = dataset.rasterband(2).unwrap().no_data_value(); + assert_eq!(nodata, Some(255.0)); + }) + .unwrap(); + } +} From 7036c43c80c2cce000fa234dbbdc3c67a657307a Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:51:42 +0800 Subject: [PATCH 08/13] refactor(sedona-gdal): prefer imported mem helpers --- c/sedona-gdal/src/gdal.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/c/sedona-gdal/src/gdal.rs b/c/sedona-gdal/src/gdal.rs index 246d647d4..bbef472f8 100644 --- a/c/sedona-gdal/src/gdal.rs +++ b/c/sedona-gdal/src/gdal.rs @@ -28,6 +28,7 @@ use crate::driver::{Driver, DriverManager}; use crate::errors::Result; use crate::gdal_api::GdalApi; use crate::gdal_dyn_bindgen::{GDALOpenFlags, OGRFieldType}; +use crate::mem::create_mem_dataset; use crate::raster::polygonize::{polygonize, PolygonizeOptions}; use crate::raster::rasterize::{rasterize, RasterizeOptions}; use crate::raster::rasterize_affine::rasterize_affine; @@ -173,7 +174,7 @@ impl Gdal { /// /// For a higher-level builder that also attaches zero-copy external bands, /// geo-transforms, projections, and nodata values, see - /// [`MemDatasetBuilder`](crate::mem::MemDatasetBuilder). + /// [`MemDatasetBuilder`]. pub fn create_mem_dataset( &self, width: usize, @@ -181,7 +182,7 @@ impl Gdal { n_owned_bands: usize, owned_bands_data_type: GdalDataType, ) -> Result { - crate::mem::create_mem_dataset( + create_mem_dataset( self.api, width, height, From 55dd466425ac8a64cbd62a2e56e2c6cc4faf24db Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Fri, 13 Mar 2026 14:37:41 +0800 Subject: [PATCH 09/13] refactor(sedona-gdal): remove facade re-export aliases --- c/sedona-gdal/src/gdal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/c/sedona-gdal/src/gdal.rs b/c/sedona-gdal/src/gdal.rs index bbef472f8..881cc39d9 100644 --- a/c/sedona-gdal/src/gdal.rs +++ b/c/sedona-gdal/src/gdal.rs @@ -30,15 +30,15 @@ use crate::gdal_api::GdalApi; use crate::gdal_dyn_bindgen::{GDALOpenFlags, OGRFieldType}; use crate::mem::create_mem_dataset; use crate::raster::polygonize::{polygonize, PolygonizeOptions}; +use crate::raster::rasterband::RasterBand; use crate::raster::rasterize::{rasterize, RasterizeOptions}; use crate::raster::rasterize_affine::rasterize_affine; use crate::raster::types::DatasetOptions; use crate::raster::types::GdalDataType; -use crate::raster::RasterBand; use crate::spatial_ref::SpatialRef; use crate::vector::feature::FieldDefn; use crate::vector::geometry::Geometry; -use crate::vector::Layer; +use crate::vector::layer::Layer; use crate::vrt::VrtDataset; use crate::vsi; From 319f31015780a09470bdd9a5ff25a594f8d8ad11 Mon Sep 17 00:00:00 2001 From: Kontinuation Date: Thu, 19 Mar 2026 22:51:00 +0800 Subject: [PATCH 10/13] docs(sedona-gdal): tighten facade API comments --- c/sedona-gdal/src/gdal.rs | 46 +++++++++++++------------------------ c/sedona-gdal/src/global.rs | 8 ++----- c/sedona-gdal/src/mem.rs | 46 ++++++++----------------------------- 3 files changed, 28 insertions(+), 72 deletions(-) diff --git a/c/sedona-gdal/src/gdal.rs b/c/sedona-gdal/src/gdal.rs index 881cc39d9..e1afbd1f5 100644 --- a/c/sedona-gdal/src/gdal.rs +++ b/c/sedona-gdal/src/gdal.rs @@ -51,7 +51,7 @@ pub struct Gdal { } impl Gdal { - /// Create a new `Gdal` instance wrapping the given API reference. + /// Create a `Gdal` wrapper for the given API reference. pub(crate) fn new(api: &'static GdalApi) -> Self { Self { api } } @@ -63,26 +63,21 @@ impl Gdal { self.api.name() } - /// Query GDAL version information. - /// - /// `request` is one of the standard `GDALVersionInfo` keys: - /// - `"RELEASE_NAME"` — e.g. `"3.8.4"` - /// - `"VERSION_NUM"` — e.g. `"3080400"` - /// - `"BUILD_INFO"` — multi-line build details + /// Fetch GDAL version information for a standard request key. pub fn version_info(&self, request: &str) -> String { self.api.version_info(request) } // -- Config -------------------------------------------------------------- - /// Set a GDAL library configuration option with **thread-local** scope. + /// Set a thread-local GDAL configuration option. pub fn set_thread_local_config_option(&self, key: &str, value: &str) -> Result<()> { config::set_thread_local_config_option(self.api, key, value) } // -- Driver -------------------------------------------------------------- - /// Look up a GDAL driver by its short name (e.g. `"GTiff"`, `"MEM"`). + /// Fetch a GDAL driver by its short name. pub fn get_driver_by_name(&self, name: &str) -> Result { DriverManager::get_driver_by_name(self.api, name) } @@ -108,21 +103,21 @@ impl Gdal { ) } - /// Open a dataset using a [`DatasetOptions`] struct (georust-compatible convenience). + /// Open a dataset using a [`DatasetOptions`] struct. pub fn open_ex_with_options(&self, path: &str, options: DatasetOptions<'_>) -> Result { Dataset::open_ex_with_options(self.api, path, options) } // -- Spatial Reference --------------------------------------------------- - /// Create a new [`SpatialRef`] from a WKT string. + /// Create a spatial reference from a WKT string. pub fn spatial_ref_from_wkt(&self, wkt: &str) -> Result { SpatialRef::from_wkt(self.api, wkt) } // -- VRT ----------------------------------------------------------------- - /// Create a new empty VRT dataset with the given dimensions. + /// Create an empty VRT dataset with the given raster size. pub fn create_vrt(&self, x_size: usize, y_size: usize) -> Result { VrtDataset::create(self.api, x_size, y_size) } @@ -141,40 +136,32 @@ impl Gdal { // -- Vector -------------------------------------------------------------- - /// Create a new OGR field definition. + /// Create an OGR field definition. pub fn create_field_defn(&self, name: &str, field_type: OGRFieldType) -> Result { FieldDefn::new(self.api, name, field_type) } // -- VSI (Virtual File System) ------------------------------------------- - /// Create a new VSI in-memory file from a given buffer. - pub fn create_mem_file(&self, file_name: &str, data: Vec) -> Result<()> { + /// Create a VSI in-memory file from the given bytes. + pub fn create_mem_file(&self, file_name: &str, data: &[u8]) -> Result<()> { vsi::create_mem_file(self.api, file_name, data) } - /// Unlink (delete) a VSI in-memory file. + /// Delete a VSI in-memory file. pub fn unlink_mem_file(&self, file_name: &str) -> Result<()> { vsi::unlink_mem_file(self.api, file_name) } - /// Copy the bytes of a VSI in-memory file, taking ownership and freeing the - /// GDAL memory. + /// Copy the bytes of a VSI in-memory file, taking ownership of the GDAL buffer. pub fn get_vsi_mem_file_bytes_owned(&self, file_name: &str) -> Result> { vsi::get_vsi_mem_file_bytes_owned(self.api, file_name) } // -- Raster operations --------------------------------------------------- - /// Create a bare in-memory (MEM) GDAL dataset via `MEMDataset::Create`. - /// - /// This bypasses GDAL's open-dataset-list mutex for better concurrency. - /// The returned dataset has `n_owned_bands` bands of type - /// `owned_bands_data_type` whose pixel data is owned by GDAL. - /// - /// For a higher-level builder that also attaches zero-copy external bands, - /// geo-transforms, projections, and nodata values, see - /// [`MemDatasetBuilder`]. + /// Create a bare in-memory MEM dataset with GDAL-owned bands. + /// For a higher-level builder with external bands and metadata, use `MemDatasetBuilder`. pub fn create_mem_dataset( &self, width: usize, @@ -191,8 +178,7 @@ impl Gdal { ) } - /// Rasterize geometries with an affine transformer derived from the - /// destination dataset. + /// Rasterize geometries using the dataset geotransform as the transformer. pub fn rasterize_affine( &self, dataset: &Dataset, @@ -211,7 +197,7 @@ impl Gdal { ) } - /// Rasterize geometries onto a dataset. + /// Rasterize geometries into the selected dataset bands. pub fn rasterize( &self, dataset: &Dataset, diff --git a/c/sedona-gdal/src/global.rs b/c/sedona-gdal/src/global.rs index c09d6c76e..5b9581163 100644 --- a/c/sedona-gdal/src/global.rs +++ b/c/sedona-gdal/src/global.rs @@ -204,12 +204,8 @@ where Ok(func(api)) } -/// Execute a closure with a high-level [`Gdal`] handle backed by the -/// process-global [`GdalApi`]. -/// -/// This is the ergonomic entry-point for most GDAL operations. The global API -/// is initialized lazily on the first call; subsequent calls reuse the same -/// `&'static GdalApi` under the hood. +/// Execute a closure with the process-global high-level [`Gdal`] handle. +/// The global API is initialized lazily on first use and then reused. pub fn with_global_gdal(func: F) -> Result where F: FnOnce(&Gdal) -> R, diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs index 16ab649a8..9c2701697 100644 --- a/c/sedona-gdal/src/mem.rs +++ b/c/sedona-gdal/src/mem.rs @@ -95,7 +95,7 @@ pub struct MemDatasetBuilder { } impl MemDatasetBuilder { - /// Create a new builder for a MEM dataset with the given dimensions. + /// Create a builder for a MEM dataset with the given dimensions. pub fn new(width: usize, height: usize) -> Self { Self { width, @@ -108,7 +108,7 @@ impl MemDatasetBuilder { } } - /// Create a new builder for a MEM dataset with the given dimensions and number of owned bands. + /// Create a builder for a MEM dataset with GDAL-owned bands. pub fn new_with_owned_bands( width: usize, height: usize, @@ -126,13 +126,8 @@ impl MemDatasetBuilder { } } - /// Create a MEM dataset with owned bands. - /// - /// This is a convenience shortcut equivalent to - /// `MemDatasetBuilder::new_with_owned_bands(...).build(gdal)`. - /// - /// Unlike [`build`](Self::build), this method is safe because datasets created - /// with only owned bands do not reference any external memory. + /// Create a MEM dataset with GDAL-owned bands. + /// This is a safe shortcut for `new_with_owned_bands(...).build(gdal)`. pub fn create( gdal: &Gdal, width: usize, @@ -149,8 +144,7 @@ impl MemDatasetBuilder { } /// Add a zero-copy band from a raw data pointer. - /// - /// Uses default pixel and line offsets (contiguous, row-major layout). + /// Use default contiguous row-major pixel and line offsets. /// /// # Safety /// @@ -163,15 +157,6 @@ impl MemDatasetBuilder { /// Add a zero-copy band with custom offsets and optional nodata. /// - /// # Arguments - /// * `data_type` - The GDAL data type of the band. - /// * `data_ptr` - Pointer to the band pixel data. - /// * `pixel_offset` - Byte offset between consecutive pixels. `None` defaults to - /// the byte size of `data_type`. - /// * `line_offset` - Byte offset between consecutive lines. `None` defaults to - /// `pixel_offset * width`. - /// * `nodata` - Optional nodata value for the band. - /// /// # Safety /// /// The caller must ensure `data_ptr` points to a valid buffer of sufficient size @@ -195,24 +180,19 @@ impl MemDatasetBuilder { self } - /// Set the geo-transform for the dataset. - /// - /// The array is `[origin_x, pixel_width, rotation_x, origin_y, rotation_y, pixel_height]`. + /// Set the dataset geotransform coefficients. pub fn geo_transform(mut self, gt: [f64; 6]) -> Self { self.geo_transform = Some(gt); self } - /// Set the projection (CRS) for the dataset as a WKT or PROJ string. + /// Set the dataset projection definition string. pub fn projection(mut self, wkt: impl Into) -> Self { self.projection = Some(wkt.into()); self } - /// Build the GDAL MEM dataset. - /// - /// Creates an empty MEM dataset using [`create_mem_dataset`], then attaches - /// bands, sets the geo-transform, projection, and per-band nodata values. + /// Build the MEM dataset and attach the configured bands and metadata. /// /// # Safety /// @@ -263,14 +243,8 @@ impl MemDatasetBuilder { } } -/// Create a bare in-memory (MEM) GDAL dataset via `MEMDataset::Create`. -/// -/// This bypasses GDAL's open-dataset-list mutex for better concurrency. -/// The returned dataset has `n_owned_bands` bands of type -/// `owned_bands_data_type` whose pixel data is owned by GDAL. -/// -/// For a higher-level builder that also attaches zero-copy external bands, -/// geo-transforms, projections, and nodata values, see [`MemDatasetBuilder`]. +/// Create a bare in-memory MEM dataset with GDAL-owned bands. +/// For a higher-level builder with external bands and metadata, use `MemDatasetBuilder`. pub(crate) fn create_mem_dataset( api: &'static GdalApi, width: usize, From 43b3ba5fa8fda0b6bc63d3ef66ec82ee3e5b4751 Mon Sep 17 00:00:00 2001 From: Kontinuation Date: Thu, 26 Mar 2026 22:37:07 +0800 Subject: [PATCH 11/13] refactor(sedona-gdal): use owned dataset constructors --- c/sedona-gdal/src/dataset.rs | 7 ------- c/sedona-gdal/src/mem.rs | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs index c2a1d2fd4..36832d866 100644 --- a/c/sedona-gdal/src/dataset.rs +++ b/c/sedona-gdal/src/dataset.rs @@ -38,17 +38,10 @@ pub struct Dataset { c_dataset: GDALDatasetH, } -<<<<<<< HEAD // SAFETY: `Dataset` has unique ownership of its GDAL dataset handle and only moves // that ownership across threads. The handle is closed exactly once on drop, and this // wrapper does not provide shared concurrent access, so `Send` is sound while `Sync` // remains intentionally unimplemented. -======= -// SAFETY: `Dataset` carries an opaque GDAL dataset handle plus an ownership flag. -// Moving the wrapper across threads only transfers ownership of that handle; it does -// not permit concurrent shared access. The handle is closed at most once on drop when -// `owned` is true, so `Send` is sound while `Sync` remains intentionally unimplemented. ->>>>>>> b522280f (test(sedona-gdal): add layer iteration coverage) unsafe impl Send for Dataset {} impl Drop for Dataset { diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs index 9c2701697..bd0570126 100644 --- a/c/sedona-gdal/src/mem.rs +++ b/c/sedona-gdal/src/mem.rs @@ -270,7 +270,7 @@ pub(crate) fn create_mem_dataset( if handle.is_null() { return Err(api.last_cpl_err(CE_Failure as u32)); } - Ok(Dataset::new_owned(api, handle)) + Ok(Dataset::new(api, handle)) } #[cfg(all(test, feature = "gdal-sys"))] From 82b17acd1d1cc6849ae0c658e73da044a232f5c1 Mon Sep 17 00:00:00 2001 From: kontinuation Date: Wed, 1 Apr 2026 13:17:30 +0800 Subject: [PATCH 12/13] Remove ignored example since the interfaces are subject to change --- c/sedona-gdal/src/mem.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs index bd0570126..487fc1090 100644 --- a/c/sedona-gdal/src/mem.rs +++ b/c/sedona-gdal/src/mem.rs @@ -20,27 +20,6 @@ //! [`MemDatasetBuilder`] provides a fluent, type-safe API for constructing GDAL MEM //! datasets with zero-copy band attachment, optional geo-transform, projection, and //! per-band nodata values. -//! -//! # Example -//! -//! ```rust,ignore -//! use sedona_gdal::global::with_global_gdal; -//! use sedona_gdal::mem::{MemDatasetBuilder, Nodata}; -//! use sedona_gdal::GdalDataType; -//! -//! with_global_gdal(|gdal| { -//! let data: Vec = vec![0u8; 256 * 256]; -//! let dataset = unsafe { -//! MemDatasetBuilder::new(256, 256) -//! .add_band(GdalDataType::UInt8, data.as_ptr()) -//! .geo_transform([0.0, 1.0, 0.0, 0.0, 0.0, -1.0]) -//! .projection("EPSG:4326") -//! .build(gdal) -//! .unwrap() -//! }; -//! assert_eq!(dataset.raster_count(), 1); -//! }).unwrap(); -//! ``` use crate::dataset::Dataset; use crate::errors::Result; From 440910778581e27fe4b39450eea9a55dfdadba6f Mon Sep 17 00:00:00 2001 From: kontinuation Date: Wed, 1 Apr 2026 14:22:02 +0800 Subject: [PATCH 13/13] fix(sedona-gdal): require mutable MEM band buffers --- c/sedona-gdal/src/dataset.rs | 4 +-- c/sedona-gdal/src/mem.rs | 64 +++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs index 36832d866..966f67982 100644 --- a/c/sedona-gdal/src/dataset.rs +++ b/c/sedona-gdal/src/dataset.rs @@ -300,11 +300,11 @@ impl Dataset { /// /// # Safety /// - /// `data_ptr` must point to valid band data that outlives this dataset. + /// `data_ptr` must point to valid mutable band data that outlives this dataset. pub unsafe fn add_band_with_data( &self, data_type: RustGdalDataType, - data_ptr: *const u8, + data_ptr: *mut u8, pixel_offset: Option, line_offset: Option, ) -> Result<()> { diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs index 487fc1090..e86acbe23 100644 --- a/c/sedona-gdal/src/mem.rs +++ b/c/sedona-gdal/src/mem.rs @@ -35,8 +35,7 @@ use crate::raster::types::GdalDataType; /// - [`i64`] for Int64 bands /// - [`u64`] for UInt64 bands /// -/// This enum encapsulates all three variants so callers don't need to match on -/// the band type when setting nodata. +/// This enum encapsulates the three nodata value representations exposed by GDAL. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Nodata { F64(f64), @@ -47,7 +46,7 @@ pub enum Nodata { /// A band specification for [`MemDatasetBuilder`]. struct MemBand { data_type: GdalDataType, - data_ptr: *const u8, + data_ptr: *mut u8, pixel_offset: Option, line_offset: Option, nodata: Option, @@ -62,7 +61,9 @@ struct MemBand { /// # Safety /// /// All `add_band*` methods are `unsafe` because the caller must ensure that the -/// provided data pointers remain valid for the lifetime of the built [`Dataset`]. +/// provided data pointers remain valid for the lifetime of the built [`Dataset`], +/// satisfy the alignment requirements of the band data type, and refer to writable +/// memory if GDAL may write through the attached `DATAPOINTER` band. pub struct MemDatasetBuilder { width: usize, height: usize, @@ -128,9 +129,9 @@ impl MemDatasetBuilder { /// # Safety /// /// The caller must ensure `data_ptr` points to a valid buffer of at least - /// `height * width * data_type.byte_size()` bytes, and that the buffer - /// outlives the built [`Dataset`]. - pub unsafe fn add_band(self, data_type: GdalDataType, data_ptr: *const u8) -> Self { + /// `height * width * data_type.byte_size()` bytes, is properly aligned for + /// `data_type`, and outlives the built [`Dataset`]. + pub unsafe fn add_band(self, data_type: GdalDataType, data_ptr: *mut u8) -> Self { self.add_band_with_options(data_type, data_ptr, None, None, None) } @@ -139,12 +140,12 @@ impl MemDatasetBuilder { /// # Safety /// /// The caller must ensure `data_ptr` points to a valid buffer of sufficient size - /// for the given dimensions and offsets, and that the buffer outlives the built - /// [`Dataset`]. + /// for the given dimensions and offsets, is properly aligned for `data_type`, and + /// outlives the built [`Dataset`]. pub unsafe fn add_band_with_options( mut self, data_type: GdalDataType, - data_ptr: *const u8, + data_ptr: *mut u8, pixel_offset: Option, line_offset: Option, nodata: Option, @@ -177,7 +178,8 @@ impl MemDatasetBuilder { /// /// This method is unsafe because the built dataset references memory provided via /// the `add_band*` methods. The caller must ensure all data pointers remain valid - /// for the lifetime of the returned [`Dataset`]. + /// for the lifetime of the returned [`Dataset`] and satisfy the alignment + /// requirements of their band data types. pub unsafe fn build(self, gdal: &Gdal) -> Result { let dataset = gdal.create_mem_dataset( self.width, @@ -261,10 +263,10 @@ mod tests { #[test] fn test_mem_builder_single_band() { with_global_gdal(|gdal| { - let data = vec![42u8; 64 * 64]; + let mut data = vec![42u8; 64 * 64]; let dataset = unsafe { MemDatasetBuilder::new(64, 64) - .add_band(GdalDataType::UInt8, data.as_ptr()) + .add_band(GdalDataType::UInt8, data.as_mut_ptr()) .build(gdal) .unwrap() }; @@ -277,14 +279,14 @@ mod tests { #[test] fn test_mem_builder_multi_band() { with_global_gdal(|gdal| { - let band1 = vec![1u16; 32 * 32]; - let band2 = vec![2u16; 32 * 32]; - let band3 = vec![3u16; 32 * 32]; + let mut band1 = vec![1u16; 32 * 32]; + let mut band2 = vec![2u16; 32 * 32]; + let mut band3 = vec![3u16; 32 * 32]; let dataset = unsafe { MemDatasetBuilder::new(32, 32) - .add_band(GdalDataType::UInt16, band1.as_ptr() as *const u8) - .add_band(GdalDataType::UInt16, band2.as_ptr() as *const u8) - .add_band(GdalDataType::UInt16, band3.as_ptr() as *const u8) + .add_band(GdalDataType::UInt16, band1.as_mut_ptr() as *mut u8) + .add_band(GdalDataType::UInt16, band2.as_mut_ptr() as *mut u8) + .add_band(GdalDataType::UInt16, band3.as_mut_ptr() as *mut u8) .build(gdal) .unwrap() }; @@ -296,11 +298,11 @@ mod tests { #[test] fn test_mem_builder_with_geo_transform() { with_global_gdal(|gdal| { - let data = vec![0f32; 10 * 10]; + let mut data = vec![0f32; 10 * 10]; let gt = [100.0, 0.5, 0.0, 200.0, 0.0, -0.5]; let dataset = unsafe { MemDatasetBuilder::new(10, 10) - .add_band(GdalDataType::Float32, data.as_ptr() as *const u8) + .add_band(GdalDataType::Float32, data.as_mut_ptr() as *mut u8) .geo_transform(gt) .build(gdal) .unwrap() @@ -314,10 +316,10 @@ mod tests { #[test] fn test_mem_builder_with_projection() { with_global_gdal(|gdal| { - let data = [0u8; 8 * 8]; + let mut data = [0u8; 8 * 8]; let dataset = unsafe { MemDatasetBuilder::new(8, 8) - .add_band(GdalDataType::UInt8, data.as_ptr()) + .add_band(GdalDataType::UInt8, data.as_mut_ptr()) .projection(r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#) .build(gdal) .unwrap() @@ -331,12 +333,12 @@ mod tests { #[test] fn test_mem_builder_with_nodata() { with_global_gdal(|gdal| { - let data = [0f64; 4 * 4]; + let mut data = [0f64; 4 * 4]; let dataset = unsafe { MemDatasetBuilder::new(4, 4) .add_band_with_options( GdalDataType::Float64, - data.as_ptr() as *const u8, + data.as_mut_ptr() as *mut u8, None, None, Some(Nodata::F64(-9999.0)), @@ -364,12 +366,12 @@ mod tests { #[test] fn test_mem_builder_mixed_band_types() { with_global_gdal(|gdal| { - let band_u8 = [0u8; 8 * 8]; - let band_f64 = vec![0f64; 8 * 8]; + let mut band_u8 = [0u8; 8 * 8]; + let mut band_f64 = vec![0f64; 8 * 8]; let dataset = unsafe { MemDatasetBuilder::new(8, 8) - .add_band(GdalDataType::UInt8, band_u8.as_ptr()) - .add_band(GdalDataType::Float64, band_f64.as_ptr() as *const u8) + .add_band(GdalDataType::UInt8, band_u8.as_mut_ptr()) + .add_band(GdalDataType::Float64, band_f64.as_mut_ptr() as *mut u8) .build(gdal) .unwrap() }; @@ -409,12 +411,12 @@ mod tests { #[test] pub fn test_mem_builder_mixed_owned_and_external_bands() { with_global_gdal(|gdal| { - let external_band = [0u8; 8 * 8]; + let mut external_band = [0u8; 8 * 8]; let dataset = unsafe { MemDatasetBuilder::new_with_owned_bands(8, 8, 1, GdalDataType::Float32) .add_band_with_options( GdalDataType::UInt8, - external_band.as_ptr(), + external_band.as_mut_ptr(), None, None, Some(Nodata::U64(255)),