///|
/// Separate buffers; attached() combines them only for an attached plan.
pub(all) struct Encoded {
header : Bytes
payload : Bytes
data_file : String?
} derive(Debug, Eq)
///|
pub fn Encoded::attached(self : Encoded) -> Bytes raise NrrdError {
if self.data_file != None {
raise Invalid("detached output has separate header/payload")
}
self.header + self.payload
}
///|
fn quote(text : String) -> String {
let s = StringBuilder()
s.write_char('"')
for c in text.iter() {
if c == '"' || c == '\\' {
s.write_char('\\')
}
s.write_char(c)
}
s.write_char('"')
s.to_string()
}
///|
fn vector_text(v : Array[Double]) -> String {
"(" + v.map(fn(x) { x.to_string() }).join(",") + ")"
}
///|
fn header_bytes(
tensor : Tensor,
encoding : Encoding,
endian : Endian,
data_file : String?,
limits : Limits,
) -> Bytes raise NrrdError {
if data_file is Some(name) {
detached_name(name)
}
let m = tensor.metadata
let lines = [
"NRRD0005",
"# MoonNRRD 0.1.0",
"type: " + tensor.scalar.name(),
"dimension: " + tensor.shape.dimension().to_string(),
"sizes: " + tensor.shape.axes.map(fn(n) { n.to_string() }).join(" "),
"encoding: " +
(match encoding {
Raw => "raw"
Ascii => "ascii"
Hex => "hex"
}),
]
if encoding != Ascii && tensor.scalar.width() > 1 {
lines.push(
"endian: " +
(match endian {
Little => "little"
Big => "big"
}),
)
}
if m.content is Some(v) {
lines.push("content: " + v)
}
if !m.labels.is_empty() {
lines.push("labels: " + m.labels.map(quote).join(" "))
}
if !m.units.is_empty() {
lines.push("units: " + m.units.map(quote).join(" "))
}
if !m.kinds.is_empty() {
lines.push("kinds: " + m.kinds.join(" "))
}
if !m.spacings.is_empty() {
lines.push(
"spacings: " + m.spacings.map(fn(v) { numeric_text(v, F64) }).join(" "),
)
}
if m.space is Some(v) {
lines.push("space: " + v)
}
if m.space_dimension is Some(v) {
lines.push("space dimension: " + v.to_string())
}
if m.origin is Some(v) {
lines.push("space origin: " + vector_text(v))
}
if !m.directions.is_empty() {
lines.push(
"space directions: " +
m.directions
.map(fn(v) {
match v {
Some(a) => vector_text(a)
None => "none"
}
})
.join(" "),
)
}
// Deterministic ordering makes archives reproducible across hash implementations.
let keys : Array[String] = m.custom.keys().collect()
keys.sort()
for key in keys {
lines.push(key + ":=" + m.custom[key])
}
if data_file is Some(v) {
lines.push("data file: " + v)
}
let mut size = 1
for line in lines {
if line.length() + 1 > limits.max_header_bytes - size {
raise Limit("serialized header budget")
}
size = size + line.length() + 1
}
@utf8.encode((lines.join("\n") + "\n\n")[:])
}
///|
pub fn write_nrrd(
tensor : Tensor,
encoding~ : Encoding = Raw,
endian~ : Endian = Little,
data_file? : String,
limits~ : Limits = Limits::default(),
) -> Encoded raise NrrdError {
limits.validate()
ignore(Shape::new(tensor.shape.axes, limits~))
ignore(tensor.shape.byte_count(tensor.scalar, limits))
let header = header_bytes(tensor, encoding, endian, data_file, limits)
let payload = match encoding {
Raw => encode_raw(tensor.data, tensor.scalar, endian, limits)
Ascii => encode_ascii(tensor.data, tensor.scalar, limits)
Hex =>
encode_hex(encode_raw(tensor.data, tensor.scalar, endian, limits), limits)
}
{ header, payload, data_file, }
}