1#![allow(unsafe_code)]
3
4use std::collections::HashMap;
40use std::sync::Arc;
41
42use arrow_array::{
43 Array, ArrayRef, Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Int64Array,
44 RecordBatch, UInt8Array, UInt16Array, UInt32Array, UInt64Array,
45};
46use arrow_schema::{DataType, Field, Schema};
47
48use super::{OxiGdalError, RasterBuffer, Result};
49use crate::types::{NoDataValue, RasterDataType};
50
51impl TryFrom<&RasterBuffer> for RecordBatch {
54 type Error = OxiGdalError;
55
56 fn try_from(buf: &RasterBuffer) -> Result<Self> {
65 let n = (buf.width() * buf.height()) as usize;
66 let nodata = buf.nodata();
67
68 #[inline]
71 fn is_nd(nodata: NoDataValue, v: f64) -> bool {
72 match nodata.as_f64() {
73 None => false,
74 Some(nd) => {
75 if nd.is_nan() && v.is_nan() {
76 true
77 } else {
78 (nd - v).abs() < f64::EPSILON
79 }
80 }
81 }
82 }
83
84 let (array, arrow_dt): (ArrayRef, DataType) = match buf.data_type() {
85 RasterDataType::UInt8 => {
86 let vals: &[u8] = buf.as_slice::<u8>().map_err(|e| OxiGdalError::Internal {
87 message: e.to_string(),
88 })?;
89 let arr: UInt8Array = vals
90 .iter()
91 .map(|&v| {
92 if is_nd(nodata, f64::from(v)) {
93 None
94 } else {
95 Some(v)
96 }
97 })
98 .collect();
99 (Arc::new(arr), DataType::UInt8)
100 }
101 RasterDataType::Int8 => {
102 let vals: &[i8] = buf.as_slice::<i8>().map_err(|e| OxiGdalError::Internal {
103 message: e.to_string(),
104 })?;
105 let arr: Int8Array = vals
106 .iter()
107 .map(|&v| {
108 if is_nd(nodata, f64::from(v)) {
109 None
110 } else {
111 Some(v)
112 }
113 })
114 .collect();
115 (Arc::new(arr), DataType::Int8)
116 }
117 RasterDataType::UInt16 => {
118 let vals: &[u16] = buf.as_slice::<u16>().map_err(|e| OxiGdalError::Internal {
119 message: e.to_string(),
120 })?;
121 let arr: UInt16Array = vals
122 .iter()
123 .map(|&v| {
124 if is_nd(nodata, f64::from(v)) {
125 None
126 } else {
127 Some(v)
128 }
129 })
130 .collect();
131 (Arc::new(arr), DataType::UInt16)
132 }
133 RasterDataType::Int16 => {
134 let vals: &[i16] = buf.as_slice::<i16>().map_err(|e| OxiGdalError::Internal {
135 message: e.to_string(),
136 })?;
137 let arr: Int16Array = vals
138 .iter()
139 .map(|&v| {
140 if is_nd(nodata, f64::from(v)) {
141 None
142 } else {
143 Some(v)
144 }
145 })
146 .collect();
147 (Arc::new(arr), DataType::Int16)
148 }
149 RasterDataType::UInt32 => {
150 let vals: &[u32] = buf.as_slice::<u32>().map_err(|e| OxiGdalError::Internal {
151 message: e.to_string(),
152 })?;
153 let arr: UInt32Array = vals
154 .iter()
155 .map(|&v| {
156 if is_nd(nodata, f64::from(v)) {
157 None
158 } else {
159 Some(v)
160 }
161 })
162 .collect();
163 (Arc::new(arr), DataType::UInt32)
164 }
165 RasterDataType::Int32 => {
166 let vals: &[i32] = buf.as_slice::<i32>().map_err(|e| OxiGdalError::Internal {
167 message: e.to_string(),
168 })?;
169 let arr: Int32Array = vals
170 .iter()
171 .map(|&v| {
172 if is_nd(nodata, f64::from(v)) {
173 None
174 } else {
175 Some(v)
176 }
177 })
178 .collect();
179 (Arc::new(arr), DataType::Int32)
180 }
181 RasterDataType::UInt64 => {
182 let vals: &[u64] = buf.as_slice::<u64>().map_err(|e| OxiGdalError::Internal {
183 message: e.to_string(),
184 })?;
185 let arr: UInt64Array = vals
186 .iter()
187 .map(|&v| {
188 if is_nd(nodata, v as f64) {
191 None
192 } else {
193 Some(v)
194 }
195 })
196 .collect();
197 (Arc::new(arr), DataType::UInt64)
198 }
199 RasterDataType::Int64 => {
200 let vals: &[i64] = buf.as_slice::<i64>().map_err(|e| OxiGdalError::Internal {
201 message: e.to_string(),
202 })?;
203 let arr: Int64Array = vals
204 .iter()
205 .map(|&v| {
206 if is_nd(nodata, v as f64) {
207 None
208 } else {
209 Some(v)
210 }
211 })
212 .collect();
213 (Arc::new(arr), DataType::Int64)
214 }
215 RasterDataType::Float32 => {
216 let vals: &[f32] = buf.as_slice::<f32>().map_err(|e| OxiGdalError::Internal {
217 message: e.to_string(),
218 })?;
219 let arr: Float32Array = vals
220 .iter()
221 .map(|&v| {
222 if is_nd(nodata, f64::from(v)) {
223 None
224 } else {
225 Some(v)
226 }
227 })
228 .collect();
229 (Arc::new(arr), DataType::Float32)
230 }
231 RasterDataType::Float64 => {
232 let vals: &[f64] = buf.as_slice::<f64>().map_err(|e| OxiGdalError::Internal {
233 message: e.to_string(),
234 })?;
235 let arr: Float64Array = vals
236 .iter()
237 .map(|&v| if is_nd(nodata, v) { None } else { Some(v) })
238 .collect();
239 (Arc::new(arr), DataType::Float64)
240 }
241 RasterDataType::CFloat32 | RasterDataType::CFloat64 => {
242 return Err(OxiGdalError::NotSupported {
243 operation: format!(
244 "Arrow conversion of complex type {}",
245 buf.data_type().name()
246 ),
247 });
248 }
249 };
250
251 debug_assert_eq!(array.len(), n, "array length must equal pixel count");
252
253 let mut metadata: HashMap<String, String> = HashMap::with_capacity(3);
254 metadata.insert("width".to_string(), buf.width().to_string());
255 metadata.insert("height".to_string(), buf.height().to_string());
256 metadata.insert("data_type".to_string(), buf.data_type().name().to_string());
257
258 let field = Field::new("pixel_values", arrow_dt, true);
259 let schema = Arc::new(Schema::new_with_metadata(vec![field], metadata));
260
261 RecordBatch::try_new(schema, vec![array]).map_err(|e| OxiGdalError::Internal {
262 message: format!("Arrow RecordBatch construction failed: {e}"),
263 })
264 }
265}
266
267impl TryFrom<RecordBatch> for RasterBuffer {
270 type Error = OxiGdalError;
271
272 fn try_from(batch: RecordBatch) -> Result<Self> {
288 if batch.num_columns() != 1 {
291 return Err(OxiGdalError::InvalidParameter {
292 parameter: "batch",
293 message: format!("Expected exactly 1 column, got {}", batch.num_columns()),
294 });
295 }
296
297 let schema = batch.schema();
298 let field = schema.field(0);
299 if field.name() != "pixel_values" {
300 return Err(OxiGdalError::InvalidParameter {
301 parameter: "batch",
302 message: format!(
303 "Expected column name 'pixel_values', got '{}'",
304 field.name()
305 ),
306 });
307 }
308
309 let meta = schema.metadata();
310
311 let width: u64 = meta
312 .get("width")
313 .ok_or(OxiGdalError::InvalidParameter {
314 parameter: "batch",
315 message: "Schema metadata missing 'width' key".to_string(),
316 })?
317 .parse::<u64>()
318 .map_err(|e| OxiGdalError::InvalidParameter {
319 parameter: "batch",
320 message: format!("Schema metadata 'width' is not a valid u64: {e}"),
321 })?;
322
323 let height: u64 = meta
324 .get("height")
325 .ok_or(OxiGdalError::InvalidParameter {
326 parameter: "batch",
327 message: "Schema metadata missing 'height' key".to_string(),
328 })?
329 .parse::<u64>()
330 .map_err(|e| OxiGdalError::InvalidParameter {
331 parameter: "batch",
332 message: format!("Schema metadata 'height' is not a valid u64: {e}"),
333 })?;
334
335 let dt_name = meta
336 .get("data_type")
337 .ok_or(OxiGdalError::InvalidParameter {
338 parameter: "batch",
339 message: "Schema metadata missing 'data_type' key".to_string(),
340 })?;
341
342 let data_type = parse_data_type(dt_name)?;
343
344 let column = batch.column(0);
347 let bytes = arrow_column_to_bytes(column, data_type)?;
348
349 RasterBuffer::new(bytes, width, height, data_type, NoDataValue::None)
350 }
351}
352
353fn parse_data_type(name: &str) -> Result<RasterDataType> {
357 match name {
358 "UInt8" => Ok(RasterDataType::UInt8),
359 "Int8" => Ok(RasterDataType::Int8),
360 "UInt16" => Ok(RasterDataType::UInt16),
361 "Int16" => Ok(RasterDataType::Int16),
362 "UInt32" => Ok(RasterDataType::UInt32),
363 "Int32" => Ok(RasterDataType::Int32),
364 "UInt64" => Ok(RasterDataType::UInt64),
365 "Int64" => Ok(RasterDataType::Int64),
366 "Float32" => Ok(RasterDataType::Float32),
367 "Float64" => Ok(RasterDataType::Float64),
368 "CFloat32" | "CFloat64" => Err(OxiGdalError::NotSupported {
369 operation: format!("Arrow conversion of complex type {name}"),
370 }),
371 other => Err(OxiGdalError::InvalidParameter {
372 parameter: "data_type",
373 message: format!("Unknown data type '{other}' in schema metadata"),
374 }),
375 }
376}
377
378fn arrow_column_to_bytes(column: &dyn Array, data_type: RasterDataType) -> Result<Vec<u8>> {
382 macro_rules! downcast_to_bytes {
383 ($ArrowArray:ty, $native:ty, $column:expr) => {{
384 let arr = $column
385 .as_any()
386 .downcast_ref::<$ArrowArray>()
387 .ok_or_else(|| OxiGdalError::Internal {
388 message: format!(
389 "Expected {} array, got {:?}",
390 stringify!($ArrowArray),
391 $column.data_type()
392 ),
393 })?;
394 let mut bytes = Vec::with_capacity(arr.len() * core::mem::size_of::<$native>());
395 for i in 0..arr.len() {
396 let v: $native = if arr.is_null(i) {
397 <$native as Default>::default()
398 } else {
399 arr.value(i)
400 };
401 bytes.extend_from_slice(&v.to_ne_bytes());
402 }
403 Ok(bytes)
404 }};
405 }
406
407 match data_type {
408 RasterDataType::UInt8 => downcast_to_bytes!(UInt8Array, u8, column),
409 RasterDataType::Int8 => downcast_to_bytes!(Int8Array, i8, column),
410 RasterDataType::UInt16 => downcast_to_bytes!(UInt16Array, u16, column),
411 RasterDataType::Int16 => downcast_to_bytes!(Int16Array, i16, column),
412 RasterDataType::UInt32 => downcast_to_bytes!(UInt32Array, u32, column),
413 RasterDataType::Int32 => downcast_to_bytes!(Int32Array, i32, column),
414 RasterDataType::UInt64 => downcast_to_bytes!(UInt64Array, u64, column),
415 RasterDataType::Int64 => downcast_to_bytes!(Int64Array, i64, column),
416 RasterDataType::Float32 => downcast_to_bytes!(Float32Array, f32, column),
417 RasterDataType::Float64 => downcast_to_bytes!(Float64Array, f64, column),
418 RasterDataType::CFloat32 | RasterDataType::CFloat64 => Err(OxiGdalError::NotSupported {
419 operation: format!("Arrow conversion of complex type {}", data_type.name()),
420 }),
421 }
422}
423
424#[cfg(test)]
427mod tests {
428 #![allow(clippy::expect_used)]
429
430 use super::*;
431 use crate::buffer::RasterBuffer;
432 use crate::types::{NoDataValue, RasterDataType};
433
434 fn make_u8_4x4() -> RasterBuffer {
436 let data: Vec<u8> = (0u8..16).collect();
437 RasterBuffer::new(data, 4, 4, RasterDataType::UInt8, NoDataValue::None)
438 .expect("valid buffer")
439 }
440
441 fn make_f32_2x3() -> RasterBuffer {
443 let data: Vec<f32> = (0..6).map(|i| i as f32 * 1.5_f32).collect();
444 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
445 RasterBuffer::new(bytes, 2, 3, RasterDataType::Float32, NoDataValue::None)
446 .expect("valid buffer")
447 }
448
449 fn make_f64_2x2() -> RasterBuffer {
451 let data: Vec<f64> = vec![1.1, 2.2, 3.3, 4.4];
452 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
453 RasterBuffer::new(bytes, 2, 2, RasterDataType::Float64, NoDataValue::None)
454 .expect("valid buffer")
455 }
456
457 #[test]
461 fn test_raster_buffer_to_record_batch_u8() {
462 let buf = make_u8_4x4();
463 let batch = RecordBatch::try_from(&buf).expect("conversion should succeed");
464
465 assert_eq!(batch.num_rows(), 16);
467 assert_eq!(batch.num_columns(), 1);
468
469 assert_eq!(batch.schema().field(0).data_type(), &DataType::UInt8);
471 assert_eq!(batch.schema().field(0).name(), "pixel_values");
472
473 let col = batch
475 .column(0)
476 .as_any()
477 .downcast_ref::<UInt8Array>()
478 .expect("UInt8Array");
479 for i in 0u8..16 {
480 assert_eq!(col.value(i as usize), i, "value at index {i}");
481 assert!(!col.is_null(i as usize));
482 }
483 }
484
485 #[test]
487 fn test_raster_buffer_to_record_batch_f32_metadata() {
488 let buf = make_f32_2x3();
489 let batch = RecordBatch::try_from(&buf).expect("conversion should succeed");
490
491 let meta = batch.schema().metadata().clone();
492 assert_eq!(meta.get("width").map(String::as_str), Some("2"));
493 assert_eq!(meta.get("height").map(String::as_str), Some("3"));
494 assert_eq!(meta.get("data_type").map(String::as_str), Some("Float32"));
495
496 assert_eq!(batch.num_rows(), 6);
498 assert_eq!(batch.schema().field(0).data_type(), &DataType::Float32);
499 }
500
501 #[test]
503 fn test_record_batch_roundtrip_f64() {
504 let original = make_f64_2x2();
505 let batch = RecordBatch::try_from(&original).expect("forward conversion");
506 let recovered = RasterBuffer::try_from(batch).expect("reverse conversion");
507
508 assert_eq!(recovered.width(), original.width());
509 assert_eq!(recovered.height(), original.height());
510 assert_eq!(recovered.data_type(), RasterDataType::Float64);
511
512 for y in 0..original.height() {
513 for x in 0..original.width() {
514 let orig_val = original.get_pixel(x, y).expect("get_pixel original");
515 let rcvd_val = recovered.get_pixel(x, y).expect("get_pixel recovered");
516 assert!(
517 (orig_val - rcvd_val).abs() < f64::EPSILON,
518 "pixel ({x},{y}): expected {orig_val}, got {rcvd_val}"
519 );
520 }
521 }
522 }
523
524 #[test]
526 fn test_nodata_becomes_null() {
527 let data_f32: Vec<f32> = vec![0.0_f32, 1.5_f32, 0.0_f32];
529 let bytes: Vec<u8> = data_f32.iter().flat_map(|v| v.to_ne_bytes()).collect();
530 let buf = RasterBuffer::new(
531 bytes,
532 3,
533 1,
534 RasterDataType::Float32,
535 NoDataValue::Float(0.0),
536 )
537 .expect("valid buffer");
538
539 let batch = RecordBatch::try_from(&buf).expect("conversion should succeed");
540 let col = batch
541 .column(0)
542 .as_any()
543 .downcast_ref::<Float32Array>()
544 .expect("Float32Array");
545
546 assert_eq!(col.len(), 3);
547 assert!(col.is_null(0), "pixel 0 (nodata) should be null");
548 assert!(!col.is_null(1), "pixel 1 (1.5) should not be null");
549 assert!(col.is_null(2), "pixel 2 (nodata) should be null");
550 assert!((col.value(1) - 1.5_f32).abs() < f32::EPSILON);
551 }
552
553 #[test]
557 fn test_complex_type_returns_error() {
558 let buf = RasterBuffer::zeros(2, 2, RasterDataType::CFloat32);
560 let result = RecordBatch::try_from(&buf);
561
562 assert!(result.is_err(), "CFloat32 conversion should fail");
563 let err = result.expect_err("expected error");
564 assert!(
565 matches!(err, OxiGdalError::NotSupported { .. }),
566 "expected NotSupported, got {err:?}"
567 );
568 }
569
570 #[test]
572 fn test_complex_type_cfloat64_returns_error() {
573 let buf = RasterBuffer::zeros(2, 2, RasterDataType::CFloat64);
574 let result = RecordBatch::try_from(&buf);
575
576 assert!(result.is_err());
577 assert!(matches!(
578 result.expect_err("expected error"),
579 OxiGdalError::NotSupported { .. }
580 ));
581 }
582
583 #[test]
587 fn test_record_batch_to_buffer_wrong_schema_missing_metadata() {
588 let field = Field::new("pixel_values", DataType::UInt8, false);
590 let schema = Arc::new(Schema::new(vec![field]));
591 let array: ArrayRef = Arc::new(UInt8Array::from(vec![1u8, 2, 3, 4]));
592 let batch = RecordBatch::try_new(schema, vec![array]).expect("valid RecordBatch for test");
593
594 let result = RasterBuffer::try_from(batch);
595 assert!(result.is_err(), "missing metadata should fail");
596 assert!(
597 matches!(
598 result.expect_err("expected error"),
599 OxiGdalError::InvalidParameter { .. }
600 ),
601 "expected InvalidParameter error"
602 );
603 }
604
605 #[test]
607 fn test_record_batch_to_buffer_wrong_column_name() {
608 let mut metadata = HashMap::new();
609 metadata.insert("width".to_string(), "2".to_string());
610 metadata.insert("height".to_string(), "2".to_string());
611 metadata.insert("data_type".to_string(), "UInt8".to_string());
612
613 let field = Field::new("wrong_name", DataType::UInt8, false);
614 let schema = Arc::new(Schema::new_with_metadata(vec![field], metadata));
615 let array: ArrayRef = Arc::new(UInt8Array::from(vec![1u8, 2, 3, 4]));
616 let batch = RecordBatch::try_new(schema, vec![array]).expect("valid RecordBatch for test");
617
618 let result = RasterBuffer::try_from(batch);
619 assert!(result.is_err());
620 assert!(matches!(
621 result.expect_err("expected error"),
622 OxiGdalError::InvalidParameter { .. }
623 ));
624 }
625
626 #[test]
628 fn test_record_batch_to_buffer_too_many_columns() {
629 let mut metadata = HashMap::new();
630 metadata.insert("width".to_string(), "2".to_string());
631 metadata.insert("height".to_string(), "2".to_string());
632 metadata.insert("data_type".to_string(), "UInt8".to_string());
633
634 let field1 = Field::new("pixel_values", DataType::UInt8, false);
635 let field2 = Field::new("extra_column", DataType::UInt8, false);
636 let schema = Arc::new(Schema::new_with_metadata(vec![field1, field2], metadata));
637 let array1: ArrayRef = Arc::new(UInt8Array::from(vec![1u8, 2, 3, 4]));
638 let array2: ArrayRef = Arc::new(UInt8Array::from(vec![5u8, 6, 7, 8]));
639 let batch =
640 RecordBatch::try_new(schema, vec![array1, array2]).expect("valid RecordBatch for test");
641
642 let result = RasterBuffer::try_from(batch);
643 assert!(result.is_err());
644 assert!(matches!(
645 result.expect_err("expected error"),
646 OxiGdalError::InvalidParameter { .. }
647 ));
648 }
649
650 #[test]
654 fn test_roundtrip_i16() {
655 let data: Vec<i16> = vec![-1000_i16, 0, 1000, i16::MAX];
656 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
657 let buf = RasterBuffer::new(bytes, 2, 2, RasterDataType::Int16, NoDataValue::None)
658 .expect("valid buffer");
659
660 let batch = RecordBatch::try_from(&buf).expect("forward");
661 let recovered = RasterBuffer::try_from(batch).expect("reverse");
662
663 assert_eq!(recovered.data_type(), RasterDataType::Int16);
664 for y in 0..2u64 {
665 for x in 0..2u64 {
666 let orig = buf.get_pixel(x, y).expect("orig pixel");
667 let rcvd = recovered.get_pixel(x, y).expect("rcvd pixel");
668 assert!((orig - rcvd).abs() < f64::EPSILON);
669 }
670 }
671 }
672
673 #[test]
675 fn test_roundtrip_u64() {
676 let data: Vec<u64> = vec![0u64, 1, u64::MAX / 2, 1_000_000];
677 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
678 let buf = RasterBuffer::new(bytes, 2, 2, RasterDataType::UInt64, NoDataValue::None)
679 .expect("valid buffer");
680
681 let batch = RecordBatch::try_from(&buf).expect("forward");
682 let recovered = RasterBuffer::try_from(batch).expect("reverse");
683
684 assert_eq!(recovered.data_type(), RasterDataType::UInt64);
685 assert_eq!(recovered.as_bytes(), buf.as_bytes());
687 }
688
689 #[test]
691 fn test_integer_nodata_becomes_null() {
692 let data: Vec<i32> = vec![-9999_i32, 100, -9999, 200];
694 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
695 let buf = RasterBuffer::new(
696 bytes,
697 4,
698 1,
699 RasterDataType::Int32,
700 NoDataValue::Integer(-9999),
701 )
702 .expect("valid buffer");
703
704 let batch = RecordBatch::try_from(&buf).expect("conversion");
705 let col = batch
706 .column(0)
707 .as_any()
708 .downcast_ref::<Int32Array>()
709 .expect("Int32Array");
710
711 assert!(col.is_null(0), "index 0 (-9999) should be null");
712 assert!(!col.is_null(1), "index 1 (100) should not be null");
713 assert!(col.is_null(2), "index 2 (-9999) should be null");
714 assert!(!col.is_null(3), "index 3 (200) should not be null");
715 assert_eq!(col.value(1), 100);
716 assert_eq!(col.value(3), 200);
717 }
718}