nifti-reader-js
Advanced tools
Comparing version 0.5.4 to 0.6.0
@@ -13,3 +13,4 @@ { | ||
"dependencies": { | ||
"pako": "*" | ||
"pako": "*", | ||
"fflate": "*" | ||
}, | ||
@@ -16,0 +17,0 @@ "devDependencies": { |
{ | ||
"name": "nifti-reader-js", | ||
"version": "0.5.4", | ||
"version": "0.6.0", | ||
"description": "A JavaScript NIfTI file format reader.", | ||
@@ -10,9 +10,9 @@ "main": "src/nifti.js", | ||
"dependencies": { | ||
"pako": "*" | ||
"fflate": "*" | ||
}, | ||
"devDependencies": { | ||
"browserify": "*", | ||
"jsdoc-to-markdown": "*", | ||
"mocha": "*", | ||
"browserify": "*", | ||
"uglifyjs": "*", | ||
"jsdoc-to-markdown": "*" | ||
"uglifyjs": "^2.4.11" | ||
}, | ||
@@ -19,0 +19,0 @@ "scripts": { |
# NIFTI-Reader-JS | ||
A JavaScript [NIfTI](http://nifti.nimh.nih.gov/) file format reader. This reader supports both NIfTI-1 and NIfT1-2 file formats, both compressed (.nii.gz) and uncompressed (.nii). | ||
###Usage | ||
[API](https://github.com/rii-mango/NIFTI-Reader-JS/wiki/API) and [more examples](https://github.com/rii-mango/NIFTI-Reader-JS/tree/master/tests) | ||
### Usage | ||
[API](https://github.com/rii-mango/NIFTI-Reader-JS/wiki/API), [drawing to canvas example](https://github.com/rii-mango/NIFTI-Reader-JS/blob/master/tests/canvas.html) and [more](https://github.com/rii-mango/NIFTI-Reader-JS/tree/master/tests) | ||
@@ -28,3 +28,3 @@ ```javascript | ||
###Install | ||
### Install | ||
Get a packaged source file: | ||
@@ -47,3 +47,3 @@ | ||
###Testing | ||
### Testing | ||
``` | ||
@@ -53,3 +53,3 @@ npm test | ||
###Building | ||
### Building | ||
See the [release folder](https://github.com/rii-mango/NIFTI-Reader-JS/tree/master/release) for the latest builds or build it yourself using: | ||
@@ -65,3 +65,3 @@ ``` | ||
NIFTI-Reader-JS makes use of the following third-party libraries: | ||
- [pako](https://github.com/nodeca/pako) — for GZIP inflating | ||
- [fflate](https://github.com/101arrowz/fflate) — for GZIP inflating | ||
@@ -16,8 +16,7 @@ | ||
nifti.NIFTI2 = nifti.NIFTI2 || ((typeof require !== 'undefined') ? require('./nifti2.js') : null); | ||
nifti.NIFTIEXTENSION = nifti.NIFTIEXTENSION || ((typeof require !== 'undefined') ? require('./nifti-extension.js') : null); | ||
nifti.Utils = nifti.Utils || ((typeof require !== 'undefined') ? require('./utilities.js') : null); | ||
var pako = pako || ((typeof require !== 'undefined') ? require('pako') : null); | ||
var fflate = fflate || ((typeof require !== 'undefined') ? require('fflate') : null); | ||
/*** Static Methods ***/ | ||
@@ -30,5 +29,4 @@ | ||
*/ | ||
nifti.isNIFTI1 = function (data) { | ||
nifti.isNIFTI1 = function (data, isHdrImgPairOK = false) { | ||
var buf, mag1, mag2, mag3; | ||
if (data.byteLength < nifti.NIFTI1.STANDARD_HEADER_SIZE) { | ||
@@ -46,2 +44,6 @@ return false; | ||
if ((isHdrImgPairOK) && (mag1 === nifti.NIFTI1.MAGIC_NUMBER2[0]) && (mag2 === nifti.NIFTI1.MAGIC_NUMBER2[1]) && | ||
(mag3 === nifti.NIFTI1.MAGIC_NUMBER2[2])) | ||
return true; // hdr/img pair | ||
return !!((mag1 === nifti.NIFTI1.MAGIC_NUMBER[0]) && (mag2 === nifti.NIFTI1.MAGIC_NUMBER[1]) && | ||
@@ -57,3 +59,3 @@ (mag3 === nifti.NIFTI1.MAGIC_NUMBER[2])); | ||
*/ | ||
nifti.isNIFTI2 = function (data) { | ||
nifti.isNIFTI2 = function (data, isHdrImgPairOK = false) { | ||
var buf, mag1, mag2, mag3; | ||
@@ -70,2 +72,6 @@ | ||
if ((isHdrImgPairOK) && (mag1 === nifti.NIFTI2.MAGIC_NUMBER2[0]) && (mag2 === nifti.NIFTI2.MAGIC_NUMBER2[1]) && | ||
(mag3 === nifti.NIFTI2.MAGIC_NUMBER2[2])) | ||
return true; // hdr/img pair | ||
return !!((mag1 === nifti.NIFTI2.MAGIC_NUMBER[0]) && (mag2 === nifti.NIFTI2.MAGIC_NUMBER[1]) && | ||
@@ -82,4 +88,4 @@ (mag3 === nifti.NIFTI2.MAGIC_NUMBER[2])); | ||
*/ | ||
nifti.isNIFTI = function (data) { | ||
return (nifti.isNIFTI1(data) || nifti.isNIFTI2(data)); | ||
nifti.isNIFTI = function (data, isHdrImgPairOK = false) { | ||
return (nifti.isNIFTI1(data, isHdrImgPairOK) || nifti.isNIFTI2(data, isHdrImgPairOK)); | ||
}; | ||
@@ -123,3 +129,3 @@ | ||
nifti.decompress = function (data) { | ||
return pako.inflate(data).buffer; | ||
return fflate.decompressSync(new Uint8Array(data)).buffer; | ||
}; | ||
@@ -134,3 +140,3 @@ | ||
*/ | ||
nifti.readHeader = function (data) { | ||
nifti.readHeader = function (data, isHdrImgPairOK = false) { | ||
var header = null; | ||
@@ -142,5 +148,5 @@ | ||
if (nifti.isNIFTI1(data)) { | ||
if (nifti.isNIFTI1(data, isHdrImgPairOK)) { | ||
header = new nifti.NIFTI1(); | ||
} else if (nifti.isNIFTI2(data)) { | ||
} else if (nifti.isNIFTI2(data, isHdrImgPairOK)) { | ||
header = new nifti.NIFTI2(); | ||
@@ -221,3 +227,3 @@ } | ||
return data.slice(loc + 8, loc + size - 8); | ||
return data.slice(loc + 8, loc + size); // +8 for loc and -8 for esize and ecode | ||
}; | ||
@@ -224,0 +230,0 @@ |
@@ -7,2 +7,4 @@ | ||
const NIFTIEXTENSION = require('./nifti-extension.js'); | ||
/*** Imports ***/ | ||
@@ -12,5 +14,4 @@ | ||
nifti.Utils = nifti.Utils || ((typeof require !== 'undefined') ? require('./utilities.js') : null); | ||
nifti.NIFTIEXTENSION = nifti.NIFTIEXTENSION || ((typeof require !== 'undefined') ? require('./nifti-extension.js') : null); | ||
/*** Constructor ***/ | ||
@@ -59,2 +60,3 @@ | ||
* @property {number} extensionCode | ||
* @property {nifti.NIFTIEXTENSION[]} extensions | ||
* @type {Function} | ||
@@ -101,2 +103,3 @@ */ | ||
this.extensionCode = 0; | ||
this.extensions = []; | ||
}; | ||
@@ -227,2 +230,10 @@ | ||
this.quatern_d = nifti.Utils.getFloatAt(rawData, 264, this.littleEndian); | ||
// Added by znshje on 27/11/2021 | ||
// | ||
// quatern_a is a parameter in quaternion [a, b, c, d], which is required in affine calculation (METHOD 2) | ||
// mentioned in the nifti1.h file | ||
// It can be calculated by a = sqrt(1.0-(b*b+c*c+d*d)) | ||
this.quatern_a = Math.sqrt(1.0 - | ||
(Math.pow(this.quatern_b, 2) + Math.pow(this.quatern_c, 2) + Math.pow(this.quatern_d, 2))) | ||
this.qoffset_x = nifti.Utils.getFloatAt(rawData, 268, this.littleEndian); | ||
@@ -232,7 +243,98 @@ this.qoffset_y = nifti.Utils.getFloatAt(rawData, 272, this.littleEndian); | ||
for (ctrOut = 0; ctrOut < 3; ctrOut += 1) { | ||
for (ctrIn = 0; ctrIn < 4; ctrIn += 1) { | ||
index = 280 + (((ctrOut * 4) + ctrIn) * 4); | ||
this.affine[ctrOut][ctrIn] = nifti.Utils.getFloatAt(rawData, index, this.littleEndian); | ||
// Added by znshje on 27/11/2021 | ||
// | ||
/* See: https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h */ | ||
if (this.qform_code > 0) { | ||
// METHOD 2 (used when qform_code > 0, which should be the "normal" case): | ||
// --------------------------------------------------------------------- | ||
// The (x,y,z) coordinates are given by the pixdim[] scales, a rotation | ||
// matrix, and a shift. This method is intended to represent | ||
// "scanner-anatomical" coordinates, which are often embedded in the | ||
// image header (e.g., DICOM fields (0020,0032), (0020,0037), (0028,0030), | ||
// and (0018,0050)), and represent the nominal orientation and location of | ||
// the data. This method can also be used to represent "aligned" | ||
// coordinates, which would typically result from some post-acquisition | ||
// alignment of the volume to a standard orientation (e.g., the same | ||
// subject on another day, or a rigid rotation to true anatomical | ||
// orientation from the tilted position of the subject in the scanner). | ||
// The formula for (x,y,z) in terms of header parameters and (i,j,k) is: | ||
// | ||
// [ x ] [ R11 R12 R13 ] [ pixdim[1] * i ] [ qoffset_x ] | ||
// [ y ] = [ R21 R22 R23 ] [ pixdim[2] * j ] + [ qoffset_y ] | ||
// [ z ] [ R31 R32 R33 ] [ qfac * pixdim[3] * k ] [ qoffset_z ] | ||
// | ||
// The qoffset_* shifts are in the NIFTI-1 header. Note that the center | ||
// of the (i,j,k)=(0,0,0) voxel (first value in the dataset array) is | ||
// just (x,y,z)=(qoffset_x,qoffset_y,qoffset_z). | ||
// | ||
// The rotation matrix R is calculated from the quatern_* parameters. | ||
// This calculation is described below. | ||
// | ||
// The scaling factor qfac is either 1 or -1. The rotation matrix R | ||
// defined by the quaternion parameters is "proper" (has determinant 1). | ||
// This may not fit the needs of the data; for example, if the image | ||
// grid is | ||
// i increases from Left-to-Right | ||
// j increases from Anterior-to-Posterior | ||
// k increases from Inferior-to-Superior | ||
// Then (i,j,k) is a left-handed triple. In this example, if qfac=1, | ||
// the R matrix would have to be | ||
// | ||
// [ 1 0 0 ] | ||
// [ 0 -1 0 ] which is "improper" (determinant = -1). | ||
// [ 0 0 1 ] | ||
// | ||
// If we set qfac=-1, then the R matrix would be | ||
// | ||
// [ 1 0 0 ] | ||
// [ 0 -1 0 ] which is proper. | ||
// [ 0 0 -1 ] | ||
// | ||
// This R matrix is represented by quaternion [a,b,c,d] = [0,1,0,0] | ||
// (which encodes a 180 degree rotation about the x-axis). | ||
// Define a, b, c, d for coding covenience | ||
const a = this.quatern_a | ||
const b = this.quatern_b | ||
const c = this.quatern_c | ||
const d = this.quatern_d | ||
this.qfac = this.pixDims[0] === 0 ? 1 : this.pixDims[0] | ||
this.quatern_R = [ | ||
[a * a + b * b - c * c - d * d, 2 * b * c - 2 * a * d, 2 * b * d + 2 * a * c], | ||
[2 * b * c + 2 * a * d, a * a + c * c - b * b - d * d, 2 * c * d - 2 * a * b], | ||
[2 * b * d - 2 * a * c, 2 * c * d + 2 * a * b, a * a + d * d - c * c - b * b] | ||
] | ||
for (ctrOut = 0; ctrOut < 3; ctrOut += 1) { | ||
for (ctrIn = 0; ctrIn < 3; ctrIn += 1) { | ||
this.affine[ctrOut][ctrIn] = this.quatern_R[ctrOut][ctrIn] * this.pixDims[ctrIn + 1]; | ||
if (ctrIn === 2) { | ||
this.affine[ctrOut][ctrIn] *= this.qfac; | ||
} | ||
} | ||
} | ||
// The last row of affine matrix is the offset vector | ||
this.affine[0][3] = this.qoffset_x | ||
this.affine[1][3] = this.qoffset_y | ||
this.affine[2][3] = this.qoffset_z | ||
} else if (this.sform_code > 0) { | ||
// METHOD 3 (used when sform_code > 0): | ||
// ----------------------------------- | ||
// The (x,y,z) coordinates are given by a general affine transformation | ||
// of the (i,j,k) indexes: | ||
// | ||
// x = srow_x[0] * i + srow_x[1] * j + srow_x[2] * k + srow_x[3] | ||
// y = srow_y[0] * i + srow_y[1] * j + srow_y[2] * k + srow_y[3] | ||
// z = srow_z[0] * i + srow_z[1] * j + srow_z[2] * k + srow_z[3] | ||
// | ||
// The srow_* vectors are in the NIFTI_1 header. Note that no use is | ||
// made of pixdim[] in this method. | ||
for (ctrOut = 0; ctrOut < 3; ctrOut += 1) { | ||
for (ctrIn = 0; ctrIn < 4; ctrIn += 1) { | ||
index = 280 + (((ctrOut * 4) + ctrIn) * 4); | ||
this.affine[ctrOut][ctrIn] = nifti.Utils.getFloatAt(rawData, index, this.littleEndian); | ||
} | ||
} | ||
} | ||
@@ -255,6 +357,13 @@ | ||
this.extensionFlag[3] = nifti.Utils.getByteAt(rawData, 348 + 3); | ||
if (this.extensionFlag[0]) { | ||
// read our extensions | ||
this.extensions = nifti.Utils.getExtensionsAt(rawData, | ||
this.getExtensionLocation(), | ||
this.littleEndian, | ||
this.vox_offset); | ||
if (this.extensionFlag[0]) { | ||
this.extensionSize = this.getExtensionSize(rawData); | ||
this.extensionCode = this.getExtensionCode(rawData); | ||
// set the extensionSize and extensionCode from the first extension found | ||
this.extensionSize = this.extensions[0].esize; | ||
this.extensionCode = this.extensions[0].ecode; | ||
} | ||
@@ -855,4 +964,2 @@ } | ||
/** | ||
@@ -867,4 +974,139 @@ * Returns the extension code. | ||
/** | ||
* Adds an extension | ||
* @param {NIFTIEXTENSION} extension | ||
* @param {number} index | ||
*/ | ||
nifti.NIFTI1.prototype.addExtension = function(extension, index = -1) { | ||
if(index == -1) { | ||
this.extensions.push(extension); | ||
} | ||
else { | ||
this.extensions.splice(index, 0, extension); | ||
} | ||
this.vox_offset += extension.esize; | ||
} | ||
/** | ||
* Removes an extension | ||
* @param {number} index | ||
*/ | ||
nifti.NIFTI1.prototype.removeExtension = function(index) { | ||
let extension = this.extensions[index]; | ||
if(extension) { | ||
this.vox_offset -= extension.esize; | ||
} | ||
this.extensions.splice(index, 1); | ||
} | ||
/** | ||
* Returns header as ArrayBuffer. | ||
* @param {boolean} includeExtensions - should extension bytes be included | ||
* @returns {ArrayBuffer} | ||
*/ | ||
nifti.NIFTI1.prototype.toArrayBuffer = function(includeExtensions = false) { | ||
const SHORT_SIZE = 2; | ||
const FLOAT32_SIZE = 4; | ||
let byteSize = 348 + 4; // + 4 for the extension bytes | ||
// calculate necessary size | ||
if(includeExtensions) { | ||
for(let extension of this.extensions) { | ||
byteSize += extension.esize; | ||
} | ||
} | ||
let byteArray = new Uint8Array(byteSize); | ||
let view = new DataView(byteArray.buffer); | ||
// sizeof_hdr | ||
view.setInt32(0, 348, this.littleEndian); | ||
// data_type, db_name, extents, session_error, regular are not used | ||
// dim_info | ||
view.setUint8(39, this.dim_info); | ||
// dims | ||
for(let i = 0; i < 8; i++) { | ||
view.setUint16(40 + SHORT_SIZE * i, this.dims[i], this.littleEndian); | ||
} | ||
// intent_p1, intent_p2, intent_p3 | ||
view.setFloat32(56, this.intent_p1, this.littleEndian); | ||
view.setFloat32(60, this.intent_p2, this.littleEndian); | ||
view.setFloat32(64, this.intent_p3, this.littleEndian); | ||
// intent_code, datatype, bitpix, slice_start | ||
view.setInt16(68, this.intent_code, this.littleEndian); | ||
view.setInt16(70, this.datatypeCode, this.littleEndian); | ||
view.setInt16(72, this.numBitsPerVoxel, this.littleEndian); | ||
view.setInt16(74, this.slice_start, this.littleEndian); | ||
// pixdim[8], vox_offset, scl_slope, scl_inter | ||
for(let i = 0; i < 8; i++) { | ||
view.setFloat32(76 + FLOAT32_SIZE * i, this.pixDims[i], this.littleEndian); | ||
} | ||
view.setFloat32(108, this.vox_offset, this.littleEndian); | ||
view.setFloat32(112, this.scl_slope, this.littleEndian); | ||
view.setFloat32(116, this.scl_inter, this.littleEndian); | ||
// slice_end | ||
view.setInt16(120, this.slice_end, this.littleEndian); | ||
// slice_code, xyzt_units | ||
view.setUint8(122, this.slice_code); | ||
view.setUint8(123, this.xyzt_units); | ||
// cal_max, cal_min, slice_duration, toffset | ||
view.setFloat32(124, this.cal_max, this.littleEndian); | ||
view.setFloat32(128, this.cal_min, this.littleEndian); | ||
view.setFloat32(132, this.slice_duration, this.littleEndian); | ||
view.setFloat32(136, this.toffset, this.littleEndian); | ||
// glmax, glmin are unused | ||
// descrip and aux_file | ||
byteArray.set(Buffer.from(this.description), 148); | ||
byteArray.set(Buffer.from(this.aux_file), 228); | ||
// qform_code, sform_code | ||
view.setInt16(252, this.qform_code, this.littleEndian); | ||
view.setInt16(254, this.sform_code, this.littleEndian); | ||
// quatern_b, quatern_c, quatern_d, qoffset_x, qoffset_y, qoffset_z, srow_x[4], srow_y[4], and srow_z[4] | ||
view.setFloat32(256, this.quatern_b, this.littleEndian); | ||
view.setFloat32(260, this.quatern_c, this.littleEndian); | ||
view.setFloat32(264, this.quatern_d, this.littleEndian); | ||
view.setFloat32(268, this.qoffset_x, this.littleEndian); | ||
view.setFloat32(272, this.qoffset_y, this.littleEndian); | ||
view.setFloat32(276, this.qoffset_z, this.littleEndian); | ||
const flattened = this.affine.flat(); | ||
// we only want the first three rows | ||
for(let i = 0; i < 12; i++) { | ||
view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], this.littleEndian); | ||
} | ||
// intent_name and magic | ||
byteArray.set(Buffer.from(this.intent_name), 328); | ||
byteArray.set(Buffer.from(this.magic), 344); | ||
// add our extension data | ||
if(includeExtensions) { | ||
byteArray.set(Uint8Array.from([1, 0, 0, 0]), 348); | ||
let extensionByteIndex = this.getExtensionLocation(); | ||
for(const extension of this.extensions) { | ||
view.setInt32(extensionByteIndex, extension.esize, extension.littleEndian); | ||
view.setInt32(extensionByteIndex + 4, extension.ecode, extension.littleEndian); | ||
byteArray.set(new Uint8Array(extension.edata), extensionByteIndex + 8); | ||
extensionByteIndex += extension.esize; | ||
} | ||
} | ||
else { | ||
// In a .nii file, these 4 bytes will always be present | ||
byteArray.set(new Uint8Array(4).fill(0), 348); | ||
} | ||
return byteArray.buffer; | ||
}; | ||
/*** Exports ***/ | ||
@@ -871,0 +1113,0 @@ |
@@ -12,4 +12,4 @@ | ||
nifti.NIFTI1 = nifti.NIFTI1 || ((typeof require !== 'undefined') ? require('./nifti1.js') : null); | ||
nifti.NIFTIEXTENSION = nifti.NIFTIEXTENSION || ((typeof require !== 'undefined') ? require('./nifti-extension.js') : null); | ||
/*** Constructor ***/ | ||
@@ -55,2 +55,3 @@ | ||
* @property {number[]} extensionFlag | ||
* @property {nifti.NIFTIEXTENSION[]} extensions | ||
* @type {Function} | ||
@@ -94,2 +95,3 @@ */ | ||
this.extensionFlag = [0, 0, 0, 0]; | ||
this.extensions = []; | ||
}; | ||
@@ -104,2 +106,3 @@ | ||
nifti.NIFTI2.MAGIC_NUMBER = [0x6E, 0x2B, 0x32, 0, 0x0D, 0x0A, 0x1A, 0x0A]; // n+2\0 | ||
nifti.NIFTI2.MAGIC_NUMBER2 = [0x6E, 0x69, 0x32, 0, 0x0D, 0x0A, 0x1A, 0x0A]; // ni2\0 | ||
@@ -131,3 +134,3 @@ | ||
} | ||
this.magic = nifti.Utils.getStringAt(rawData, 4, 12); | ||
this.datatypeCode = nifti.Utils.getShortAt(rawData, 12, this.littleEndian); | ||
@@ -204,4 +207,11 @@ this.numBitsPerVoxel = nifti.Utils.getShortAt(rawData, 14, this.littleEndian); | ||
if (this.extensionFlag[0]) { | ||
this.extensionSize = this.getExtensionSize(rawData); | ||
this.extensionCode = this.getExtensionCode(rawData); | ||
// read our extensions | ||
this.extensions = nifti.Utils.getExtensionsAt(rawData, | ||
this.getExtensionLocation(), | ||
this.littleEndian, | ||
this.vox_offset); | ||
// set the extensionSize and extensionCode from the first extension found | ||
this.extensionSize = this.extensions[0].esize; | ||
this.extensionCode = this.extensions[0].ecode; | ||
} | ||
@@ -316,4 +326,2 @@ } | ||
/** | ||
@@ -326,4 +334,17 @@ * Returns the extension code. | ||
/** | ||
* Adds an extension | ||
* @param {NIFTIEXTENSION} extension | ||
* @param {number} index | ||
*/ | ||
nifti.NIFTI2.prototype.addExtension = nifti.NIFTI1.prototype.addExtension; | ||
/** | ||
* Removes an extension | ||
* @param {number} index | ||
*/ | ||
nifti.NIFTI2.prototype.removeExtension = nifti.NIFTI1.prototype.removeExtension; | ||
/** | ||
@@ -397,4 +418,146 @@ * Returns a human-readable string of datatype. | ||
/** | ||
* Returns header as ArrayBuffer. | ||
* @param {boolean} includeExtensions - should extension bytes be included | ||
* @returns {ArrayBuffer} | ||
*/ | ||
nifti.NIFTI2.prototype.toArrayBuffer = function(includeExtensions = false) { | ||
const INT64_SIZE = 8; | ||
const DOUBLE_SIZE = 8; | ||
let byteSize = 540 + 4; // +4 for extension bytes | ||
// calculate necessary size | ||
if(includeExtensions) { | ||
for(let extension of this.extensions) { | ||
byteSize += extension.esize; | ||
} | ||
} | ||
let byteArray = new Uint8Array(byteSize); | ||
let view = new DataView(byteArray.buffer); | ||
// sizeof_hdr | ||
view.setInt32(0, 540, this.littleEndian); | ||
// magic | ||
byteArray.set(Buffer.from(this.magic), 4); | ||
// datatype | ||
view.setInt16(12, this.datatypeCode, this.littleEndian); | ||
// bitpix | ||
view.setInt16(14, this.numBitsPerVoxel, this.littleEndian); | ||
// dim[8] | ||
for(let i = 0; i < 8; i++) { | ||
view.setBigInt64(16 + INT64_SIZE * i, BigInt(this.dims[i]), this.littleEndian); | ||
} | ||
// intent_p1 | ||
view.setFloat64(80, this.intent_p1, this.littleEndian); | ||
// intent_p2 | ||
view.setFloat64(88, this.intent_p2, this.littleEndian); | ||
// intent_p3 | ||
view.setFloat64(96, this.intent_p3, this.littleEndian); | ||
// pixdim | ||
for(let i = 0; i < 8; i++) { | ||
view.setFloat64(104 + DOUBLE_SIZE * i, this.pixDims[i], this.littleEndian); | ||
} | ||
// vox_offset | ||
view.setBigInt64(168, BigInt(this.vox_offset), this.littleEndian); | ||
// scl_slope | ||
view.setFloat64(176, this.scl_slope, this.littleEndian); | ||
// scl_inter | ||
view.setFloat64(184, this.scl_inter, this.littleEndian); | ||
// cal_max | ||
view.setFloat64(192, this.cal_max, this.littleEndian); | ||
// cal_min | ||
view.setFloat64(200, this.cal_min, this.littleEndian); | ||
// slice_duration | ||
view.setFloat64(208, this.slice_duration, this.littleEndian); | ||
// toffset | ||
view.setFloat64(216, this.toffset, this.littleEndian); | ||
// slice_start | ||
view.setBigInt64(224, BigInt(this.slice_start), this.littleEndian); | ||
// slice end | ||
view.setBigInt64(232, BigInt(this.slice_end), this.littleEndian); | ||
// descrip | ||
byteArray.set(Buffer.from(this.description), 240); | ||
// aux_file | ||
byteArray.set(Buffer.from(this.aux_file), 320); | ||
// qform_code | ||
view.setInt32(344, this.qform_code, this.littleEndian); | ||
// sform_code | ||
view.setInt32(348, this.sform_code, this.littleEndian); | ||
// quatern_b | ||
view.setFloat64(352, this.quatern_b, this.littleEndian); | ||
// quatern_c | ||
view.setFloat64(360, this.quatern_c, this.littleEndian); | ||
// quatern_d | ||
view.setFloat64(368, this.quatern_d, this.littleEndian); | ||
// qoffset_x | ||
view.setFloat64(376, this.qoffset_x, this.littleEndian); | ||
// qoffset_y | ||
view.setFloat64(384, this.qoffset_y, this.littleEndian); | ||
// qoffset_z | ||
view.setFloat64(392, this.qoffset_z, this.littleEndian); | ||
// srow_x[4], srow_y[4], and srow_z[4] | ||
const flattened = this.affine.flat(); | ||
// we only want the first three rows | ||
for(let i = 0; i < 12; i++) { | ||
view.setFloat64(400 + DOUBLE_SIZE * i, flattened[i], this.littleEndian); | ||
} | ||
// slice_code | ||
view.setInt32(496, this.slice_code, this.littleEndian); | ||
// xyzt_units | ||
view.setInt32(500, this.xyzt_units, this.littleEndian); | ||
// intent_code | ||
view.setInt32(504, this.intent_code, this.littleEndian); | ||
// intent_name | ||
byteArray.set(Buffer.from(this.intent_name), 508); | ||
// dim_info | ||
view.setUint8(524, this.dim_info); | ||
// add our extension data | ||
if(includeExtensions) { | ||
byteArray.set(Uint8Array.from([1, 0, 0, 0]), 540); | ||
let extensionByteIndex = this.getExtensionLocation(); | ||
for(const extension of this.extensions) { | ||
view.setInt32(extensionByteIndex, extension.esize, extension.littleEndian); | ||
view.setInt32(extensionByteIndex + 4, extension.ecode, extension.littleEndian); | ||
byteArray.set(new Uint8Array(extension.edata), extensionByteIndex + 8); | ||
extensionByteIndex += extension.esize; | ||
} | ||
} | ||
else { | ||
// In a .nii file, these 4 bytes will always be present | ||
byteArray.set(new Uint8Array(4).fill(0), 540); | ||
} | ||
return byteArray.buffer; | ||
} | ||
/*** Exports ***/ | ||
@@ -401,0 +564,0 @@ |
@@ -11,5 +11,5 @@ | ||
nifti.Utils = nifti.Utils || {}; | ||
nifti.NIFTIEXTENSION = nifti.NIFTIEXTENSION || ((typeof require !== 'undefined') ? require('./nifti-extension.js') : null); | ||
/*** Static Pseudo-constants ***/ | ||
@@ -85,4 +85,42 @@ | ||
nifti.Utils.getExtensionsAt = function (data, start, littleEndian, voxOffset) { | ||
let extensions = []; | ||
let extensionByteIndex = start; | ||
// Multiple extended header sections are allowed | ||
while(extensionByteIndex < voxOffset ) { | ||
// assume same endianess as header until proven otherwise | ||
let extensionLittleEndian = littleEndian; | ||
let esize = nifti.Utils.getIntAt(data, extensionByteIndex, littleEndian); | ||
if(!esize) { | ||
break; // no more extensions | ||
} | ||
// check if this takes us past vox_offset | ||
if(esize + extensionByteIndex > voxOffset) { | ||
// check if reversing byte order gets a proper size | ||
extensionLittleEndian = !extensionLittleEndian; | ||
esize = nifti.Utils.getIntAt(data, extensionByteIndex, extensionLittleEndian); | ||
if(esize + extensionByteIndex > voxOffset) { | ||
throw new Error('This does not appear to be a valid NIFTI extension'); | ||
} | ||
} | ||
// esize must be a positive integral multiple of 16 | ||
if(esize % 16 != 0) { | ||
throw new Error("This does not appear to be a NIFTI extension"); | ||
} | ||
let ecode = nifti.Utils.getIntAt(data, extensionByteIndex + 4, extensionLittleEndian); | ||
let edata = data.buffer.slice(extensionByteIndex + 8, extensionByteIndex + esize); | ||
console.log('extensionByteIndex: ' + (extensionByteIndex + 8) + ' esize: ' + esize); | ||
console.log(edata); | ||
let extension = new nifti.NIFTIEXTENSION(esize, ecode, edata, extensionLittleEndian); | ||
extensions.push(extension); | ||
extensionByteIndex += esize; | ||
} | ||
return extensions; | ||
} | ||
nifti.Utils.toArrayBuffer = function (buffer) { | ||
@@ -89,0 +127,0 @@ var ab, view, i; |
@@ -12,3 +12,3 @@ | ||
var buf = fs.readFileSync('./tests/data/5D.nii'); | ||
var buf = fs.readFileSync('./tests/data/5D_zeros.nii.gz'); | ||
var data = nifti.Utils.toArrayBuffer(buf); | ||
@@ -19,3 +19,14 @@ var nifti1 = null; | ||
describe('NIFTI-Reader-JS', function () { | ||
describe('uncompressed nifti-1 test', function () { | ||
describe('compressed 5D nifti-1 test', function () { | ||
it('isCompressed() should return true', function () { | ||
assert.equal(true, nifti.isCompressed(data)); | ||
}); | ||
it('should not throw error when decompressing', function (done) { | ||
assert.doesNotThrow(function() { | ||
data = nifti.decompress(data); | ||
done(); | ||
}); | ||
}); | ||
it('should not throw error when reading header', function (done) { | ||
@@ -52,3 +63,9 @@ assert.doesNotThrow(function() { | ||
}); | ||
it('image data checksum should equal 1033497386', function () { | ||
var imageData = nifti.readImage(nifti1, data); | ||
var checksum = nifti.Utils.crc32(new DataView(imageData)); | ||
assert.equal(checksum, 2980574675); | ||
}); | ||
}); | ||
}); |
@@ -12,5 +12,8 @@ | ||
// var buf = fs.readFileSync('./tests/data/afni.nii.gz'); | ||
var buf = fs.readFileSync('./tests/data/with_extension.nii.gz'); | ||
var data = nifti.Utils.toArrayBuffer(buf); | ||
var nifti1 = null; | ||
var extension = null; | ||
const EXPECTED_EXTENSION_LENGTH = 376; | ||
@@ -41,6 +44,86 @@ describe('NIFTI-Reader-JS', function () { | ||
it('extension length should be 368', function () { | ||
assert.equal(368, nifti.readExtensionData(nifti1, data).byteLength); | ||
it('extension length should be 376 (384 - 8)', function () { | ||
assert.equal(EXPECTED_EXTENSION_LENGTH + 8, nifti1.getExtensionSize(new DataView(data))); | ||
assert.equal(EXPECTED_EXTENSION_LENGTH, nifti.readExtensionData(nifti1, data).byteLength); | ||
}); | ||
it('should have one extension that is 376 bytes', function() { | ||
extension = nifti1.extensions[0]; | ||
assert.equal(EXPECTED_EXTENSION_LENGTH, extension.edata.byteLength); | ||
assert.equal(1, nifti1.extensions.length); | ||
}); | ||
it('removed extension changes the vox offset', function() { | ||
extension = nifti1.extensions[0]; | ||
assert.equal(EXPECTED_EXTENSION_LENGTH, extension.edata.byteLength); | ||
assert.equal(1, nifti1.extensions.length); | ||
}); | ||
it('removed extension updates the vox offset', function() { | ||
let oldVoxOffset = nifti1.vox_offset; | ||
nifti1.removeExtension(0); | ||
assert.equal(0, nifti1.extensions.length); | ||
assert.equal(nifti1.vox_offset + extension.esize, oldVoxOffset); | ||
}); | ||
it('added extension updates vox_offset', function() { | ||
let oldVoxOffset = nifti1.vox_offset; | ||
nifti1.addExtension(extension); | ||
assert.equal(1, nifti1.extensions.length); | ||
assert.equal(nifti1.vox_offset, oldVoxOffset + extension.esize); | ||
}); | ||
it('toArrayBuffer properly allocates extension byte array', function() { | ||
assert.equal(1, nifti1.extensions.length); | ||
let bytesWithHeader = nifti1.toArrayBuffer(true); | ||
let bytesWithoutHeader = nifti1.toArrayBuffer(); | ||
let headerBytesGreater = bytesWithHeader.byteLength > bytesWithoutHeader.byteLength; | ||
assert.equal(true, headerBytesGreater); | ||
}); | ||
it('toArrayBuffer properly preserves extension bytes', function() { | ||
let bytes = nifti1.toArrayBuffer(true); | ||
let copy = nifti.readHeader(bytes); | ||
assert.equal(1, copy.extensions.length); | ||
assert.equal(EXPECTED_EXTENSION_LENGTH, copy.extensions[0].edata.byteLength); | ||
}); | ||
it('extensions can be added and serialized', function() { | ||
let edata = new Int32Array(6); | ||
edata.fill(8); | ||
let newExtension = new nifti.NIFTIEXTENSION(32, 4, edata.buffer, true); | ||
nifti1.addExtension(newExtension); | ||
assert.equal(2, nifti1.extensions.length); | ||
let bytes = nifti1.toArrayBuffer(true); | ||
let copy = nifti.readHeader(bytes); | ||
assert.equal(2, copy.extensions.length); | ||
assert.equal(4, copy.extensions[1].ecode); | ||
assert.equal(24, copy.extensions[1].edata.byteLength); | ||
}); | ||
it('extensions can be removed by index', function() { | ||
nifti1.removeExtension(1); | ||
assert.equal(1, nifti1.extensions.length); | ||
let bytes = nifti1.toArrayBuffer(true); | ||
let copy = nifti.readHeader(bytes); | ||
assert.equal(1, copy.extensions.length); | ||
assert.equal(EXPECTED_EXTENSION_LENGTH, copy.extensions[0].edata.byteLength); | ||
}) | ||
it('extensions can be inserted and serialized', function() { | ||
let newExtension = new nifti.NIFTIEXTENSION(32, 4, new Uint8Array(16), true); | ||
nifti1.addExtension(newExtension, 0); | ||
assert.equal(2, nifti1.extensions.length); | ||
let bytes = nifti1.toArrayBuffer(true); | ||
let copy = nifti.readHeader(bytes); | ||
assert.equal(2, copy.extensions.length); | ||
assert.equal(4, copy.extensions[0].ecode); | ||
assert.equal(32, copy.extensions[0].esize); | ||
assert.equal(24, copy.extensions[0].edata.byteLength); | ||
}) | ||
}); | ||
}); |
@@ -15,2 +15,4 @@ | ||
var nifti1 = null; | ||
var bytes = null; | ||
var clone = null; | ||
@@ -55,3 +57,11 @@ describe('NIFTI-Reader-JS', function () { | ||
}); | ||
it('data returned from toArrayBuffer preserves all nifti-1 properties', function() { | ||
nifti1 = nifti.readHeader(data); | ||
bytes = nifti1.toArrayBuffer(); | ||
clone = nifti.readHeader(bytes); | ||
assert.deepEqual(clone, nifti1); | ||
}); | ||
}); | ||
}); |
@@ -15,2 +15,4 @@ | ||
var nifti2 = null; | ||
var bytes = null; | ||
var clone = null; | ||
@@ -62,3 +64,10 @@ describe('NIFTI-Reader-JS', function () { | ||
}); | ||
it('data returned from toArrayBuffer preserves all nifti-2 properties', function() { | ||
bytes = nifti2.toArrayBuffer(); | ||
clone = nifti.readHeader(bytes); | ||
assert.deepEqual(clone, nifti2); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
39
3420665
8469
16
2
+ Addedfflate@*
+ Addedfflate@0.8.2(transitive)
- Removedpako@*
- Removedpako@2.1.0(transitive)