use std::io::{self, Write};
use std::path::Path;
pub fn write_u32(
path: &Path,
data: &[u32],
width: usize,
height: usize,
depth: usize,
) -> io::Result<()> {
assert_eq!(
data.len(),
width * height * depth,
"data does not match the shape"
);
let shape = if depth > 1 {
format!("({depth}, {height}, {width})")
} else {
format!("({height}, {width})")
};
let header = format!("{{'descr': '<u4', 'fortran_order': False, 'shape': {shape}, }}");
let unpadded = 10 + header.len() + 1;
let padding = (64 - unpadded % 64) % 64;
let header = format!("{header}{}\n", " ".repeat(padding));
let mut file = std::fs::File::create(path)?;
file.write_all(b"\x93NUMPY")?;
file.write_all(&[1u8, 0u8])?;
file.write_all(&(header.len() as u16).to_le_bytes())?;
file.write_all(header.as_bytes())?;
for &value in data {
file.write_all(&value.to_le_bytes())?;
}
file.flush()
}
pub fn write_f64(
path: &Path,
data: &[f64],
width: usize,
height: usize,
depth: usize,
) -> io::Result<()> {
assert_eq!(
data.len(),
width * height * depth,
"data does not match the shape"
);
let shape = if depth > 1 {
format!("({depth}, {height}, {width})")
} else {
format!("({height}, {width})")
};
let header = format!("{{'descr': '<f8', 'fortran_order': False, 'shape': {shape}, }}");
let unpadded = 10 + header.len() + 1;
let padding = (64 - unpadded % 64) % 64;
let header = format!("{header}{}\n", " ".repeat(padding));
let mut file = std::fs::File::create(path)?;
file.write_all(b"\x93NUMPY")?;
file.write_all(&[1u8, 0u8])?;
file.write_all(&(header.len() as u16).to_le_bytes())?;
file.write_all(header.as_bytes())?;
for &value in data {
file.write_all(&value.to_le_bytes())?;
}
file.flush()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_header_is_padded_to_a_multiple_of_sixty_four() {
let dir = std::env::temp_dir().join("glazier-npy-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("a.npy");
write_u32(&path, &[1, 2, 3, 4, 5, 6], 3, 2, 1).unwrap();
let bytes = std::fs::read(&path).unwrap();
let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
assert_eq!((10 + header_len) % 64, 0);
assert_eq!(bytes.len(), 10 + header_len + 6 * 4);
assert_eq!(&bytes[0..6], b"\x93NUMPY");
}
#[test]
fn a_float_array_writes_its_own_descriptor() {
let dir = std::env::temp_dir().join("glazier-npy-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("c.npy");
write_f64(&path, &[1.5, 2.5, 3.5, 4.5], 2, 2, 1).unwrap();
let bytes = std::fs::read(&path).unwrap();
let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
let header = String::from_utf8_lossy(&bytes[10..10 + header_len]);
assert!(header.contains("<f8"), "{header}");
assert_eq!(bytes.len(), 10 + header_len + 4 * 8);
}
#[test]
fn a_volume_writes_a_three_dimensional_shape() {
let dir = std::env::temp_dir().join("glazier-npy-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("b.npy");
write_u32(&path, &[0u32; 24], 2, 3, 4).unwrap();
let bytes = std::fs::read(&path).unwrap();
let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
let header = String::from_utf8_lossy(&bytes[10..10 + header_len]);
assert!(header.contains("(4, 3, 2)"), "{header}");
}
}