nrrd-js
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -0,0 +0,0 @@ var nrrd = require('./nrrd.js'); |
364
nrrd.js
@@ -16,3 +16,3 @@ "use strict"; | ||
module.exports.serialize = function (nrrdOrg) { | ||
var i, buffer, arr, totalLen = 1, nrrd = {}, prop, nativeType, nativeSize, bufferData, arrData, lines = [], header; | ||
var i, buffer, arr, totalLen = 1, nrrd = {}, prop, nativeType, nativeSize, bufferData, arrData, line, lines = [], header; | ||
@@ -72,4 +72,57 @@ // Copy nrrdOrg to nrrd to allow modifications without altering the original | ||
// Try to infer spatial dimension | ||
var spaceDimension = undefined; | ||
if (nrrd.spaceDimension!==undefined) { | ||
spaceDimension = nrrd.spaceDimension; | ||
} else if (nrrd.space!==undefined) { | ||
switch(nrrd.space) { | ||
case "right-anterior-superior": | ||
case "RAS": | ||
spaceDimension = 3; | ||
break; | ||
case "left-anterior-superior": | ||
case "LAS": | ||
spaceDimension = 3; | ||
break; | ||
case "left-posterior-superior": | ||
case "LPS": | ||
spaceDimension = 3; | ||
break; | ||
case "right-anterior-superior-time": | ||
case "RAST": | ||
spaceDimension = 4; | ||
break; | ||
case "left-anterior-superior-time": | ||
case "LAST": | ||
spaceDimension = 4; | ||
break; | ||
case "left-posterior-superior-time": | ||
case "LPST": | ||
spaceDimension = 4; | ||
break; | ||
case "scanner-xyz": | ||
spaceDimension = 3; | ||
break; | ||
case "scanner-xyz-time": | ||
spaceDimension = 4; | ||
break; | ||
case "3D-right-handed": | ||
spaceDimension = 3; | ||
break; | ||
case "3D-left-handed": | ||
spaceDimension = 3; | ||
break; | ||
case "3D-right-handed-time": | ||
spaceDimension = 4; | ||
break; | ||
case "3D-left-handed-time": | ||
spaceDimension = 4; | ||
break; | ||
default: | ||
console.warn("Unrecognized space: " + nrrd.space); | ||
} | ||
} | ||
// Now check that we have a valid nrrd structure. | ||
checkNRRD(nrrd, true); | ||
checkNRRD(nrrd); | ||
@@ -133,88 +186,17 @@ // Determine number of elements and check that we have enough data (if possible) | ||
// Put in dimension and space dimension (the NRRD spec requires that these are present before any lists whose length depends on them) | ||
var firstProps = ['dimension', 'spaceDimension', 'space']; | ||
for(i=0; i<firstProps.length; i++) { | ||
prop = firstProps[i]; | ||
if (nrrd[prop] === undefined) continue; // Skip things we explicitly set to undefined. | ||
line = serializeField(prop, nrrd[prop], nrrd.dimension, spaceDimension); | ||
if (line!==undefined) lines.push(line); | ||
} | ||
// Put in field specifications | ||
for(prop in nrrd) { | ||
if (nrrd[prop] === undefined) continue; // Skip things we explicitly set to undefined. | ||
switch(prop) { | ||
// nrrd-js stuff: skip | ||
case 'data': | ||
case 'buffer': | ||
case 'keys': | ||
case 'version': | ||
break; | ||
// Literal (uninterpreted) fields | ||
case 'content': | ||
case 'number': | ||
case 'sampleunits': | ||
lines.push(prop + ": " + nrrd[prop]); | ||
break; | ||
// Integers (no infinity or whatever, just a plain integer, so the default serialization is good enough) | ||
case 'dimension': | ||
case 'blockSize': | ||
case 'lineSkip': | ||
case 'byteSkip': | ||
assert((typeof nrrd[prop]) == "number" || nrrd[prop] instanceof Number, "Field " + prop + " should at least contain a number!"); | ||
lines.push(prop + ": " + nrrd[prop]); | ||
break; | ||
// Floats (default serialization is good enough, as NaN contains nan, ignoring case, and similarly for Infinity inf) | ||
case 'min': | ||
case 'max': | ||
case 'oldMin': | ||
case 'oldMax': | ||
assert((typeof nrrd[prop]) == "number" || nrrd[prop] instanceof Number, "Field " + prop + " should contain a number!"); | ||
lines.push(prop + ": " + nrrd[prop]); | ||
break; | ||
// Lists of strings | ||
case 'labels': | ||
case 'units': | ||
assert(nrrd[prop].length !== undefined && nrrd[prop].length == nrrd.dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
nrrd[prop].forEach(function (val) { assert((typeof val) == "string" || val instanceof String, "Field " + prop + " should be a list of numbers!"); }); | ||
lines.push(prop + ": " + nrrd[prop].map(serializeNRRDQuotedString).join(" ")); | ||
break; | ||
// Lists of integers | ||
case 'sizes': | ||
assert(nrrd[prop].length !== undefined && nrrd[prop].length == nrrd.dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
nrrd[prop].forEach(function (val) { assert((typeof val) == "number" || val instanceof Number, "Field " + prop + " should be a list of numbers!"); }); | ||
lines.push(prop + ": " + nrrd[prop].join(" ")); | ||
break; | ||
// Lists of floats | ||
case 'spacings': | ||
case 'thicknesses': | ||
case 'axisMins': | ||
case 'axisMaxs': | ||
assert(nrrd[prop].length !== undefined && nrrd[prop].length == nrrd.dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
nrrd[prop].forEach(function (val) { assert((typeof val) == "number" || val instanceof Number, "Field " + prop + " should be a list of numbers!"); }); | ||
lines.push(prop + ": " + nrrd[prop].join(" ")); | ||
break; | ||
// One-of-a-kind fields | ||
case 'type': | ||
assert((typeof nrrd[prop]) == "string" || nrrd[prop] instanceof String, "Field " + prop + " should contain a string!"); | ||
lines.push(prop + ": " + nrrd[prop]); | ||
break; | ||
case 'encoding': | ||
assert((typeof nrrd[prop]) == "string" || nrrd[prop] instanceof String, "Field " + prop + " should contain a string!"); | ||
lines.push(prop + ": " + nrrd[prop]); | ||
break; | ||
case 'endian': | ||
assert((typeof nrrd[prop]) == "string" || nrrd[prop] instanceof String, "Field " + prop + " should contain a string!"); | ||
lines.push(prop + ": " + nrrd[prop]); | ||
break; | ||
case 'dataFile': | ||
if (nrrd.dataFile.length || (nrrd.dataFile.files && 'subdim' in nrrd.dataFile)) { | ||
// List of data files: skip for now | ||
} else { | ||
lines.push(prop + ": " + serializeNRRDDataFile(nrrd[prop])); | ||
} | ||
break; | ||
case 'centers': | ||
assert(nrrd[prop].length !== undefined && nrrd[prop].length == nrrd.dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
lines.push(prop + ": " + nrrd[prop].map(serializeNRRDOptional).join(" ")); | ||
break; | ||
case 'kinds': | ||
assert(nrrd[prop].length !== undefined && nrrd[prop].length == nrrd.dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
lines.push(prop + ": " + nrrd[prop].map(serializeNRRDOptional).join(" ")); | ||
break; | ||
// Something unknown | ||
default: | ||
console.warn("Unrecognized NRRD field: " + prop + ", skipping."); | ||
} | ||
if (firstProps.indexOf(prop)>=0) continue; // Skip the fields we already output. | ||
line = serializeField(prop, nrrd[prop], nrrd.dimension, spaceDimension); | ||
if (line!==undefined) lines.push(line); | ||
} | ||
@@ -224,3 +206,3 @@ | ||
if (nrrd.keys) for(prop in nrrd.keys) { | ||
if (prop.contains(":=")) throw new Error("The combination ':=' is not allowed in an NRRD key!"); | ||
if (prop.indexOf(":=")>=0) throw new Error("The combination ':=' is not allowed in an NRRD key!"); | ||
lines.push(prop + ":=" + escapeValue(nrrd[prop])); | ||
@@ -379,2 +361,119 @@ } | ||
// Serializes NRRD fields | ||
function serializeField(prop, value, dimension, spaceDimension) { | ||
var line; | ||
var propNRRD = mapJavascriptToNRRD(prop); | ||
switch(prop) { | ||
// nrrd-js stuff: skip | ||
case 'data': | ||
case 'buffer': | ||
case 'keys': | ||
case 'version': | ||
break; | ||
// Literal (uninterpreted) fields | ||
case 'content': | ||
case 'number': | ||
case 'sampleUnits': | ||
case 'space': | ||
line = propNRRD + ": " + value; | ||
break; | ||
// Integers (no infinity or whatever, just a plain integer, so the default serialization is good enough) | ||
case 'blockSize': | ||
case 'lineSkip': | ||
case 'byteSkip': | ||
case 'dimension': | ||
case 'spaceDimension': | ||
assert((typeof value) == "number" || value instanceof Number, "Field " + prop + " should at least contain a number!"); | ||
line = propNRRD + ": " + value; | ||
break; | ||
// Floats (default serialization is good enough, as NaN contains nan, ignoring case, and similarly for Infinity inf) | ||
case 'min': | ||
case 'max': | ||
case 'oldMin': | ||
case 'oldMax': | ||
assert((typeof value) == "number" || value instanceof Number, "Field " + prop + " should contain a number!"); | ||
line = propNRRD + ": " + value; | ||
break; | ||
// Vectors | ||
case 'spaceOrigin': | ||
assert(value.length === spaceDimension, "Field " + prop + " should be a list with length equal to the space dimension!"); | ||
value.forEach(function (val) { assert((typeof val) == "number" || val instanceof Number, "Field " + prop + " should be a list of numbers!"); }); | ||
line = propNRRD + ": (" + value.join(",") + ")"; | ||
break; | ||
// Lists of strings | ||
case 'labels': | ||
case 'units': | ||
case 'spaceUnits': | ||
assert(value.length !== undefined && value.length == dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
value.forEach(function (val) { assert((typeof val) == "string" || val instanceof String, "Field " + prop + " should be a list of numbers!"); }); | ||
line = propNRRD + ": " + value.map(serializeNRRDQuotedString).join(" "); | ||
break; | ||
// Lists of integers | ||
case 'sizes': | ||
assert(value.length !== undefined && value.length == dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
value.forEach(function (val) { assert((typeof val) == "number" || val instanceof Number, "Field " + prop + " should be a list of numbers!"); }); | ||
line = propNRRD + ": " + value.join(" "); | ||
break; | ||
// Lists of floats | ||
case 'spacings': | ||
case 'thicknesses': | ||
case 'axisMins': | ||
case 'axisMaxs': | ||
assert(value.length !== undefined && value.length == dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
value.forEach(function (val) { assert((typeof val) == "number" || val instanceof Number, "Field " + prop + " should be a list of numbers!"); }); | ||
line = propNRRD + ": " + value.join(" "); | ||
break; | ||
// Lists of vectors (dimension sized) | ||
case 'spaceDirections': | ||
assert(value.length !== undefined && value.length === dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
value.forEach(function (vec) { | ||
assert(vec.length === spaceDimension, "The elements of field " + prop + " should be lists with length equal to the space dimension!"); | ||
vec.forEach(function (val) { assert((typeof val) == "number" || val instanceof Number, "The elements of field " + prop + " should be lists of numbers!"); }); | ||
}); | ||
line = propNRRD + ": " + value.map(function(vec) { return "(" + vec.join(",") + ")"; }).join(" "); | ||
break; | ||
// Lists of vectors (space dimension sized) | ||
case 'measurementFrame': | ||
assert(value.length !== undefined && value.length === spaceDimension, "Field " + prop + " should be a list with length equal to the space dimension!"); | ||
value.forEach(function (vec) { | ||
assert(vec.length === spaceDimension, "The elements of field " + prop + " should be lists with length equal to the space dimension!"); | ||
vec.forEach(function (val) { assert((typeof val) == "number" || val instanceof Number, "The elements of field " + prop + " should be lists of numbers!"); }); | ||
}); | ||
line = propNRRD + ": " + value.map(function(vec) { return "(" + vec.join(",") + ")"; }).join(" "); | ||
break; | ||
// One-of-a-kind fields | ||
case 'type': | ||
assert((typeof value) == "string" || value instanceof String, "Field " + prop + " should contain a string!"); | ||
line = propNRRD + ": " + value; | ||
break; | ||
case 'encoding': | ||
assert((typeof value) == "string" || value instanceof String, "Field " + prop + " should contain a string!"); | ||
line = propNRRD + ": " + value; | ||
break; | ||
case 'endian': | ||
assert((typeof value) == "string" || value instanceof String, "Field " + prop + " should contain a string!"); | ||
line = propNRRD + ": " + value; | ||
break; | ||
case 'dataFile': | ||
if (value.length || (value.files && 'subdim' in value)) { | ||
// List of data files: skip for now | ||
} else { | ||
line = propNRRD + ": " + serializeNRRDDataFile(value); | ||
} | ||
break; | ||
case 'centers': | ||
assert(value.length !== undefined && value.length == dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
line = propNRRD + ": " + value.map(serializeNRRDOptional).join(" "); | ||
break; | ||
case 'kinds': | ||
assert(value.length !== undefined && value.length == dimension, "Field " + prop + " should be a list with length equal to the dimension!"); | ||
line = propNRRD + ": " + value.map(serializeNRRDOptional).join(" "); | ||
break; | ||
// Something unknown | ||
default: | ||
console.warn("Unrecognized NRRD field: " + prop + ", skipping."); | ||
} | ||
return line; | ||
} | ||
// Parses and normalizes NRRD fields, assumes the field names are already lower case. | ||
@@ -386,3 +485,3 @@ function parseField(identifier, descriptor) { | ||
case 'number': | ||
case 'sampleunits': | ||
case 'sampleUnits': | ||
break; | ||
@@ -394,2 +493,3 @@ // Integers | ||
case 'byteSkip': | ||
case 'spaceDimension': | ||
descriptor = parseNRRDInteger(descriptor); | ||
@@ -402,7 +502,12 @@ break; | ||
case 'oldMax': | ||
descriptor = parseNRRDFloat(descriptor.toLowerCase()); | ||
descriptor = parseNRRDFloat(descriptor); | ||
break; | ||
// Vectors | ||
case 'spaceOrigin': | ||
descriptor = parseNRRDVector(descriptor); | ||
break; | ||
// Lists of strings | ||
case 'labels': | ||
case 'units': | ||
case 'spaceUnits': | ||
descriptor = parseNRRDWhitespaceSeparatedList(descriptor, parseNRRDQuotedString); | ||
@@ -421,2 +526,7 @@ break; | ||
break; | ||
// Lists of vectors | ||
case 'spaceDirections': | ||
case 'measurementFrame': | ||
descriptor = parseNRRDWhitespaceSeparatedList(descriptor, parseNRRDVector); | ||
break; | ||
// One-of-a-kind fields | ||
@@ -441,2 +551,5 @@ case 'type': | ||
break; | ||
case 'space': | ||
descriptor = parseNRRDSpace(descriptor); | ||
break; | ||
// Something unknown | ||
@@ -468,4 +581,16 @@ default: | ||
'centers': 'centers', // Not different, just included so it is clear why centerings maps to centers | ||
'centerings': 'centers' | ||
'centerings': 'centers', | ||
'space dimension': 'spaceDimension', | ||
'space units': 'spaceUnits', | ||
'space origin': 'spaceOrigin', | ||
'space directions': 'spaceDirections', | ||
'measurement frame': 'measurementFrame' | ||
}; | ||
var mapJavascriptToNRRDStatic = function() { | ||
var id, m = {}; | ||
for(id in mapNRRDToJavascriptStatic) { | ||
m[mapNRRDToJavascriptStatic[id]] = id; | ||
} | ||
return m; | ||
}(); | ||
function mapNRRDToJavascript(id) { | ||
@@ -479,2 +604,8 @@ // In any case, use the lower case version of the id | ||
} | ||
function mapJavascriptToNRRD(id) { | ||
// Filter out any fields for which we have an explicit NRRD name | ||
if (id in mapJavascriptToNRRDStatic) return mapJavascriptToNRRDStatic[id]; | ||
// Otherwise, just return the id | ||
return id; | ||
} | ||
@@ -487,7 +618,7 @@ function parseNRRDInteger(str) { | ||
// Note: this assumes any letters in str are in lower case! | ||
function parseNRRDFloat(str) { | ||
if (str.contains('nan')) return NaN; | ||
if (str.contains('-inf')) return -Infinity; | ||
if (str.contains('inf')) return Infinity; | ||
str = str.toLowerCase(); | ||
if (str.indexOf('nan')>=0) return NaN; | ||
if (str.indexOf('-inf')>=0) return -Infinity; | ||
if (str.indexOf('inf')>=0) return Infinity; | ||
var val = parseFloat(str); | ||
@@ -498,2 +629,7 @@ if (Number.isNaN(val)) throw new Error("Malformed NRRD float: " + str); | ||
function parseNRRDVector(str) { | ||
if (str.length<2 || str[0]!=="(" || str[str.length-1]!==")") throw new Error("Malformed NRRD vector: " + str); | ||
return str.slice(1, -1).split(",").map(parseNRRDFloat); | ||
} | ||
function parseNRRDQuotedString(str) { | ||
@@ -503,3 +639,3 @@ if (length<2 || str[0]!='"' || str[str.length-1]!='"') { | ||
} | ||
return str.substring(1, str.length-1).replace('\\"', '"'); | ||
return str.slice(1, -1).replace('\\"', '"'); | ||
} | ||
@@ -598,2 +734,40 @@ | ||
function parseNRRDSpace(space) { | ||
switch(space.toLowerCase()) { | ||
case "right-anterior-superior": | ||
case "ras": | ||
return "right-anterior-superior"; | ||
case "left-anterior-superior": | ||
case "las": | ||
return "left-anterior-superior"; | ||
case "left-posterior-superior": | ||
case "lps": | ||
return "left-posterior-superior"; | ||
case "right-anterior-superior-time": | ||
case "rast": | ||
return "right-anterior-superior-time"; | ||
case "left-anterior-superior-time": | ||
case "last": | ||
return "left-anterior-superior-time"; | ||
case "left-posterior-superior-time": | ||
case "lpst": | ||
return "left-posterior-superior-time"; | ||
case "scanner-xyz": | ||
return "scanner-xyz"; | ||
case "scanner-xyz-time": | ||
return "scanner-xyz-time"; | ||
case "3d-right-handed": | ||
return "3D-right-handed"; | ||
case "3d-left-handed": | ||
return "3D-left-handed"; | ||
case "3d-right-handed-time": | ||
return "3D-right-handed-time"; | ||
case "3d-left-handed-time": | ||
return "3D-left-handed-time"; | ||
default: | ||
console.warn("Unrecognized space: " + space); | ||
return space; | ||
} | ||
} | ||
function parseNRRDEndian(endian) { | ||
@@ -969,2 +1143,4 @@ switch(endian.toLowerCase()) { | ||
// TODO: Check space/orientation fields. | ||
// We should either have inline data or external data | ||
@@ -971,0 +1147,0 @@ if ((ret.data === undefined || ret.data.length === 0) && (ret.buffer === undefined || ret.buffer.byteLength === 0) && ret.dataFile === undefined) { |
{ | ||
"name": "nrrd-js", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Handling NRRD files in Javascript.", | ||
@@ -5,0 +5,0 @@ "main": "nrrd.js", |
@@ -0,0 +0,0 @@ NRRD support for Javascript |
38
test.js
@@ -54,3 +54,22 @@ var test = require('tape'); | ||
} | ||
var file = nrrd.parse(nrrd.serialize(nrrd.parse(fs.readFileSync('example4.nrrd')))), | ||
i, list = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]; | ||
t.equal(file.type, 'uint8'); | ||
t.equal(file.dimension, 3); | ||
t.equal(file.sizes.length, 3); | ||
t.equal(file.sizes[0], 4); | ||
t.equal(file.sizes[1], 3); | ||
t.equal(file.sizes[2], 2); | ||
t.equal(file.space, "right-anterior-superior"); | ||
t.ok(arrayEqual(file.spaceOrigin, [1.0,2.0,3.0]), "spaceOrigin should be equal to [1.0,2.0,3.0]"); | ||
t.ok(arrayEqual(file.spaceDirections, [[1.0,0.0,0.0],[0,0.5,0],[0,0,0.3]]), "spaceDirections should be equal to [[1.0,0,0],...]"); | ||
t.equal(file.data.length, 4*3*2); | ||
t.equal(file.data.byteLength, 4*3*2); | ||
for(i=0; i<list.length; i++) { | ||
t.equal(file.data[i], list[i]); | ||
} | ||
t.end(); | ||
@@ -80,4 +99,5 @@ }); | ||
arr1 = ndarray(new Uint16Array([1,2,3, 65000,64000,63000, 10000,11000,12000, 4,5,6]), [4,3]), | ||
file = nrrd.parse(nrrd.serialize({data: arr1.data, sizes: arr1.shape.slice().reverse()})), | ||
arr2 = ndarray(file.data, file.sizes.slice().reverse());; | ||
serialized = nrrd.serialize({data: arr1.data, sizes: arr1.shape.slice().reverse()}), | ||
file = nrrd.parse(serialized), | ||
arr2 = ndarray(file.data, file.sizes.slice().reverse()); | ||
@@ -94,4 +114,18 @@ t.equal(file.dimension, arr1.shape.length); | ||
} | ||
//console.log(String.fromCharCode.apply(null, new Uint8Array(serialized))); | ||
t.end(); | ||
}); | ||
function arrayEqual(a, b) { | ||
if (a.length === undefined || a.length !== b.length) return false; | ||
for(var i=0; i<a.length; i++) { | ||
if (a[i].length !== undefined) { | ||
if (!arrayEqual(a[i],b[i])) return false; | ||
} else { | ||
if (a[i]!==b[i]) return false; | ||
} | ||
} | ||
return true; | ||
} | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
839653
11
1300