//! clawhdf5's HDF5 reader for JavaScript, via `wasm-bindgen`. //! //! ```js //! import init, { open } from "./pkg/clawhdf5_wasm.js"; //! await init(); //! const file = open(new Uint8Array(await blob.arrayBuffer())); //! file.list("/"); // [{ name, kind: "group" | "dataset" }] //! file.info("/x"); // { shape, maxshape, dtype, elementShape } //! file.attrs("/x"); // [{ name, value, dtype }] //! file.read("/x"); // { shape, dtype, data: Float64Array | ... | string[] } //! file.readHyperslab("/x", [0, 0], [10, 10]); // stride, block optional //! file.free(); //! ``` //! //! Numeric data comes back in the typed array of the stored width //! (`Int16Array` for `i16`, `BigInt64Array` for `i64`, `Float32Array` for //! `f32` and `f16`, ...); strings and enumeration names as arrays of strings. //! Anything else is a thrown `Error` naming the datatype. Only the reader is //! exposed: nothing here writes files. //! //! The logic lives in [`core`], which is plain Rust and tested natively. pub mod core; use clawhdf5::AttrValue; use js_sys::{Array, Object, Reflect}; use wasm_bindgen::prelude::*; use crate::core::{Data, Hyperslab, Reader}; /// JavaScript numbers are exact up to 2^53. const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; fn js_err(msg: String) -> JsError { JsError::new(&msg) } fn set(obj: &Object, key: &str, value: impl Into) { // Defining a property on a fresh plain object cannot fail. Reflect::set(obj, &JsValue::from_str(key), &value.into()).unwrap_throw(); } fn shape_to_js(shape: &[u64]) -> Array { shape.iter().map(|&d| JsValue::from_f64(d as f64)).collect() } fn indices_from_js(what: &str, v: &[f64]) -> Result, JsError> { v.iter() .map(|&x| { if x.is_finite() && x >= 0.0 && x.fract() == 0.0 && x <= MAX_SAFE_INTEGER { Ok(x as u64) } else { Err(js_err(format!( "{what} must hold non-negative integers, got {x}" ))) } }) .collect() } fn data_to_js(data: Data) -> JsValue { match data { Data::F32(v) => js_sys::Float32Array::from(&v[..]).into(), Data::F64(v) => js_sys::Float64Array::from(&v[..]).into(), Data::I8(v) => js_sys::Int8Array::from(&v[..]).into(), Data::I16(v) => js_sys::Int16Array::from(&v[..]).into(), Data::I32(v) => js_sys::Int32Array::from(&v[..]).into(), Data::I64(v) => js_sys::BigInt64Array::from(&v[..]).into(), Data::U8(v) => js_sys::Uint8Array::from(&v[..]).into(), Data::U16(v) => js_sys::Uint16Array::from(&v[..]).into(), Data::U32(v) => js_sys::Uint32Array::from(&v[..]).into(), Data::U64(v) => js_sys::BigUint64Array::from(&v[..]).into(), Data::Strings(v) => v .into_iter() .map(|s| JsValue::from_str(&s)) .collect::() .into(), } } /// A scalar integer as a `number` when exact, else a `bigint`. fn int_to_js(x: i128) -> JsValue { if (x as f64).abs() <= MAX_SAFE_INTEGER { JsValue::from_f64(x as f64) } else if let Ok(v) = i64::try_from(x) { JsValue::from(v) } else { JsValue::from(x as u64) } } /// An attribute value, and the datatype of one that has no JavaScript form. fn attr_to_js(value: AttrValue) -> (JsValue, Option) { match value { AttrValue::F64(x) => (JsValue::from_f64(x), None), AttrValue::F64Array(v) => (js_sys::Float64Array::from(&v[..]).into(), None), AttrValue::I64(x) => (int_to_js(i128::from(x)), None), AttrValue::I64Array(v) => (js_sys::BigInt64Array::from(&v[..]).into(), None), AttrValue::U64(x) => (int_to_js(i128::from(x)), None), AttrValue::U64Array(v) => (js_sys::BigUint64Array::from(&v[..]).into(), None), AttrValue::String(s) => (JsValue::from_str(&s), None), AttrValue::StringArray(v) => ( v.iter() .map(|s| JsValue::from_str(s)) .collect::() .into(), None, ), AttrValue::Raw { datatype, .. } => (JsValue::NULL, Some(core::describe(&datatype))), } } /// An open HDF5 (or NetCDF-4) file. #[wasm_bindgen] pub struct H5File { inner: Reader, } /// Open a file from its bytes. Throws if they are not an HDF5 file. #[wasm_bindgen] pub fn open(bytes: Vec) -> Result { H5File::new(bytes) } /// The clawhdf5 version this module was built from. #[wasm_bindgen] pub fn version() -> String { env!("CARGO_PKG_VERSION").to_string() } #[wasm_bindgen] impl H5File { /// Same as [`open`]. #[wasm_bindgen(constructor)] pub fn new(bytes: Vec) -> Result { Ok(H5File { inner: Reader::open(bytes).map_err(js_err)?, }) } /// `"group"` or `"dataset"`. pub fn kind(&self, path: &str) -> Result { Ok(self.inner.kind(path).map_err(js_err)?.as_str().to_string()) } /// The group's members: `[{ name, kind }]`, groups first. pub fn list(&self, path: &str) -> Result { Ok(self .inner .list(path) .map_err(js_err)? .into_iter() .map(|c| { let o = Object::new(); set(&o, "name", c.name); set(&o, "kind", c.kind.as_str()); JsValue::from(o) }) .collect()) } /// `{ shape, maxshape, dtype, elementShape }`. `maxshape` is `null` /// when not recorded, with `null` for each unlimited dimension. pub fn info(&self, path: &str) -> Result { let i = self.inner.info(path).map_err(js_err)?; let o = Object::new(); set(&o, "shape", shape_to_js(&i.shape)); let max: JsValue = match i.maxshape { None => JsValue::NULL, Some(dims) => dims .into_iter() .map(|d| d.map_or(JsValue::NULL, |d| JsValue::from_f64(d as f64))) .collect::() .into(), }; set(&o, "maxshape", max); set(&o, "dtype", i.dtype); set(&o, "elementShape", shape_to_js(&i.element_shape)); Ok(o) } /// `[{ name, value, dtype }]`, sorted by name. Scalars are `number` /// (`bigint` beyond 2^53) or `string`; arrays are typed arrays or /// `string[]`. An attribute with no JavaScript form has `value: null` /// and its `dtype`; one that could not be read at all is reported by /// [`attrErrors`](Self::attr_errors). pub fn attrs(&self, path: &str) -> Result { let (attrs, _) = self.inner.attrs(path).map_err(js_err)?; Ok(attrs .into_iter() .map(|a| { let o = Object::new(); set(&o, "name", a.name); let (value, dtype) = attr_to_js(a.value); set(&o, "value", value); set(&o, "dtype", dtype.map_or(JsValue::NULL, JsValue::from)); JsValue::from(o) }) .collect()) } /// Messages for attributes that could not be read. #[wasm_bindgen(js_name = attrErrors)] pub fn attr_errors(&self, path: &str) -> Result { let (_, errors) = self.inner.attrs(path).map_err(js_err)?; Ok(errors.into_iter().map(JsValue::from).collect()) } /// The whole dataset: `{ shape, dtype, data }`, `data` in row-major /// order. pub fn read(&self, path: &str) -> Result { self.read_impl(path, None) } /// A regular hyperslab (`H5Sselect_hyperslab`): `stride` and `block` /// default to 1. The result's shape is `count * block` per dimension. #[wasm_bindgen(js_name = readHyperslab)] pub fn read_hyperslab( &self, path: &str, start: Vec, count: Vec, stride: Option>, block: Option>, ) -> Result { let slab = Hyperslab { start: indices_from_js("start", &start)?, count: indices_from_js("count", &count)?, stride: stride.map(|s| indices_from_js("stride", &s)).transpose()?, block: block.map(|b| indices_from_js("block", &b)).transpose()?, }; self.read_impl(path, Some(&slab)) } fn read_impl(&self, path: &str, slab: Option<&Hyperslab>) -> Result { let dtype = self.inner.info(path).map_err(js_err)?.dtype; let a = self.inner.read(path, slab).map_err(js_err)?; let o = Object::new(); set(&o, "shape", shape_to_js(&a.shape)); set(&o, "dtype", dtype); set(&o, "data", data_to_js(a.data)); Ok(o) } }