@niivue/niivue
Version:
minimal webgl2 nifti image viewer
597 lines (592 loc) • 19.3 kB
JavaScript
// src/utils/nice.ts
var nice = (x, round) => {
const exp = Math.floor(Math.log(x) / Math.log(10));
const f = x / Math.pow(10, exp);
let nf;
if (round) {
if (f < 1.5) {
nf = 1;
} else if (f < 3) {
nf = 2;
} else if (f < 7) {
nf = 5;
} else {
nf = 10;
}
} else {
if (f <= 1) {
nf = 1;
} else if (f <= 2) {
nf = 2;
} else if (f <= 5) {
nf = 5;
} else {
nf = 10;
}
}
return nf * Math.pow(10, exp);
};
// src/utils/file-utils.ts
function readFileAsDataURL(input) {
return new Promise((resolve, reject) => {
let filePromise;
if (input instanceof File) {
filePromise = Promise.resolve(input);
} else {
filePromise = new Promise((resolve2, reject2) => {
input.file(resolve2, reject2);
});
}
filePromise.then((file) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
resolve(reader.result);
} else {
reject(new Error("Expected a string from FileReader.result"));
}
};
reader.onerror = () => {
reject(reader.error ?? new Error("Unknown FileReader error"));
};
reader.readAsDataURL(file);
}).catch((err) => reject(err));
});
}
// src/nvutilities.ts
import arrayEqual from "array-equal";
import { mat4, vec3, vec4 } from "gl-matrix";
var NVUtilities = class _NVUtilities {
static arrayBufferToBase64(arrayBuffer) {
const bytes = new Uint8Array(arrayBuffer);
return _NVUtilities.uint8tob64(bytes);
}
static async decompress(data) {
const format = data[0] === 31 && data[1] === 139 && data[2] === 8 ? "gzip" : data[0] === 120 && (data[1] === 1 || data[1] === 94 || data[1] === 156 || data[1] === 218) ? "deflate" : "deflate-raw";
const stream = new DecompressionStream(format);
const writer = stream.writable.getWriter();
writer.write(data).catch(console.error);
const closePromise = writer.close().catch(console.error);
const response = new Response(stream.readable);
const result = new Uint8Array(await response.arrayBuffer());
await closePromise;
return result;
}
static async decompressToBuffer(data) {
const decompressed = await _NVUtilities.decompress(data);
return decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
}
static async readMatV4(buffer, isReplaceDots = false) {
let len = buffer.byteLength;
if (len < 40) {
throw new Error("File too small to be MAT v4: bytes = " + buffer.byteLength);
}
let reader = new DataView(buffer);
let magic = reader.getUint16(0, true);
let _buffer = buffer;
if (magic === 35615 || magic === 8075) {
const raw = await this.decompress(new Uint8Array(buffer));
reader = new DataView(raw.buffer);
magic = reader.getUint16(0, true);
_buffer = raw.buffer;
len = _buffer.byteLength;
}
const textDecoder = new TextDecoder("utf-8");
const bytes = new Uint8Array(_buffer);
let pos = 0;
const mat = {};
function getTensDigit(v) {
return Math.floor(v / 10) % 10;
}
function readArray(tagDataType, tagBytesStart, tagBytesEnd) {
const byteArray = new Uint8Array(bytes.subarray(tagBytesStart, tagBytesEnd));
if (tagDataType === 1) {
return new Float32Array(byteArray.buffer);
}
if (tagDataType === 2) {
return new Int32Array(byteArray.buffer);
}
if (tagDataType === 3) {
return new Int16Array(byteArray.buffer);
}
if (tagDataType === 4) {
return new Uint16Array(byteArray.buffer);
}
if (tagDataType === 5) {
return new Uint8Array(byteArray.buffer);
}
return new Float64Array(byteArray.buffer);
}
function readTag() {
const mtype = reader.getUint32(pos, true);
const mrows = reader.getUint32(pos + 4, true);
const ncols = reader.getUint32(pos + 8, true);
const imagf = reader.getUint32(pos + 12, true);
const namlen = reader.getUint32(pos + 16, true);
pos += 20;
if (imagf !== 0) {
throw new Error("Matlab V4 reader does not support imaginary numbers");
}
const tagArrayItems = mrows * ncols;
if (tagArrayItems < 1) {
throw new Error("mrows * ncols must be greater than one");
}
const byteArray = new Uint8Array(bytes.subarray(pos, pos + namlen));
let tagName = textDecoder.decode(byteArray).trim().replaceAll("\0", "");
if (isReplaceDots) {
tagName = tagName.replaceAll(".", "_");
}
const tagDataType = getTensDigit(mtype);
let tagBytesPerItem = 8;
if (tagDataType >= 1 && tagDataType <= 2) {
tagBytesPerItem = 4;
} else if (tagDataType >= 3 && tagDataType <= 4) {
tagBytesPerItem = 2;
} else if (tagDataType === 5) {
tagBytesPerItem = 1;
} else if (tagDataType !== 0) {
throw new Error("impossible Matlab v4 datatype");
}
pos += namlen;
if (mtype > 50) {
throw new Error("Does not appear to be little-endian V4 Matlab file");
}
const posEnd = pos + tagArrayItems * tagBytesPerItem;
mat[tagName] = readArray(tagDataType, pos, posEnd);
pos = posEnd;
}
while (pos + 20 < len) {
readTag();
}
return mat;
}
// readMatV4()
static b64toUint8(base64) {
const binaryString = atob(base64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
/*
https://gist.github.com/jonleighton/958841
MIT LICENSE
Copyright 2011 Jon Leighton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
static uint8tob64(bytes) {
let base64 = "";
const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const byteLength = bytes.byteLength;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
let a, b, c, d;
let chunk;
for (let i = 0; i < mainLength; i = i + 3) {
chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
a = (chunk & 16515072) >> 18;
b = (chunk & 258048) >> 12;
c = (chunk & 4032) >> 6;
d = chunk & 63;
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
if (byteRemainder === 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2;
b = (chunk & 3) << 4;
base64 += encodings[a] + encodings[b] + "==";
} else if (byteRemainder === 2) {
chunk = bytes[mainLength] << 8 | bytes[mainLength + 1];
a = (chunk & 64512) >> 10;
b = (chunk & 1008) >> 4;
c = (chunk & 15) << 2;
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
}
return base64;
}
// https://stackoverflow.com/questions/34156282/how-do-i-save-json-to-local-text-file
static download(content, fileName, contentType) {
const a = document.createElement("a");
const contentArray = Array.isArray(content) ? content : [content];
const file = new Blob(contentArray, { type: contentType });
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
}
static readFileAsync(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
static blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
static async decompressBase64String(base64) {
const compressed = atob(base64);
const compressedBuffer = new ArrayBuffer(compressed.length);
const compressedView = new Uint8Array(compressedBuffer);
for (let i = 0; i < compressed.length; i++) {
compressedView[i] = compressed.charCodeAt(i);
}
return _NVUtilities.decompressArrayBuffer(compressedView);
}
static async compressToBase64String(string) {
const buf = await _NVUtilities.compressStringToArrayBuffer(string);
return _NVUtilities.uint8tob64(new Uint8Array(buf));
}
/**
* Converts a string into a Uint8Array for use with compression/decompression methods (101arrowz/fflate: MIT License)
* @param str The string to encode
* @param latin1 Whether or not to interpret the data as Latin-1. This should
* not need to be true unless decoding a binary string.
* @returns The string encoded in UTF-8/Latin-1 binary
*/
static strToU8(str, latin1) {
if (latin1) {
const ar2 = new Uint8Array(str.length);
for (let i = 0; i < str.length; ++i) {
ar2[i] = str.charCodeAt(i);
}
return ar2;
}
const l = str.length;
const slc = (v, s, e) => {
if (s == null || s < 0) {
s = 0;
}
if (e == null || e > v.length) {
e = v.length;
}
return new Uint8Array(v.subarray(s, e));
};
let ar = new Uint8Array(str.length + (str.length >> 1));
let ai = 0;
const w = (v) => {
ar[ai++] = v;
};
for (let i = 0; i < l; ++i) {
if (ai + 5 > ar.length) {
const n = new Uint8Array(ai + 8 + (l - i << 1));
n.set(ar);
ar = n;
}
let c = str.charCodeAt(i);
if (c < 128 || latin1) {
w(c);
} else if (c < 2048) {
w(192 | c >> 6);
w(128 | c & 63);
} else if (c > 55295 && c < 57344) {
c = 65536 + (c & 1023 << 10) | str.charCodeAt(++i) & 1023;
w(240 | c >> 18);
w(128 | c >> 12 & 63);
w(128 | c >> 6 & 63);
w(128 | c & 63);
} else {
c = 65536 + (c & 1023 << 10) | str.charCodeAt(++i) & 1023;
w(240 | c >> 18);
w(128 | c >> 12 & 63);
w(128 | c >> 6 & 63);
w(128 | c & 63);
}
}
return slc(ar, 0, ai);
}
static async compress(data, format = "gzip") {
const stream = new CompressionStream(format);
const writer = stream.writable.getWriter();
writer.write(data).catch(console.error);
const closePromise = writer.close().catch(console.error);
const response = new Response(stream.readable);
const result = await response.arrayBuffer();
await closePromise;
return result;
}
static async compressStringToArrayBuffer(input) {
const uint8Array = this.strToU8(input);
return await this.compress(uint8Array);
}
static isArrayBufferCompressed(buffer) {
if (buffer && buffer.byteLength) {
const arr = new Uint8Array(buffer);
const magicNumber = arr[0] << 8 | arr[1];
return magicNumber === 8075;
} else {
return false;
}
}
/**
* Converts a Uint8Array to a string (101arrowz/fflate: MIT License)
* @param dat The data to decode to string
* @param latin1 Whether or not to interpret the data as Latin-1. This should
* not need to be true unless encoding to binary string.
* @returns The original UTF-8/Latin-1 string
*/
static strFromU8(dat, latin1) {
if (latin1) {
let r = "";
for (let i = 0; i < dat.length; i += 16384) {
r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
}
return r;
} else {
const slc = (v, s2, e) => {
if (s2 == null || s2 < 0) {
s2 = 0;
}
if (e == null || e > v.length) {
e = v.length;
}
return new Uint8Array(v.subarray(s2, e));
};
const dutf8 = (d) => {
for (let r2 = "", i = 0; ; ) {
let c = d[i++];
const eb = (c > 127) + (c > 223) + (c > 239);
if (i + eb > d.length) {
return { s: r2, r: slc(d, i - 1) };
}
if (!eb) {
r2 += String.fromCharCode(c);
} else if (eb === 3) {
c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536;
r2 += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
} else if (eb & 1) {
r2 += String.fromCharCode((c & 31) << 6 | d[i++] & 63);
} else {
r2 += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63);
}
}
};
const { s, r } = dutf8(dat);
if (r.length) {
throw new Error("Unexpected trailing bytes in UTF-8 decoding");
}
return s;
}
}
static async decompressArrayBuffer(buffer) {
const decompressed = await this.decompress(new Uint8Array(buffer));
return this.strFromU8(decompressed);
}
static arraysAreEqual(a, b) {
return arrayEqual(a, b);
}
/**
* Generate a pre-filled number array.
*
* @param start - start value
* @param stop - stop value
* @param step - step value
* @returns filled number array
*/
static range(start, stop, step) {
return Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
}
/**
* convert spherical AZIMUTH, ELEVATION to Cartesian
* @param azimuth - azimuth number
* @param elevation - elevation number
* @returns the converted [x, y, z] coordinates
* @example
* xyz = NVUtilities.sph2cartDeg(42, 42)
*/
static sph2cartDeg(azimuth, elevation) {
const Phi = -elevation * (Math.PI / 180);
const Theta = (azimuth - 90) % 360 * (Math.PI / 180);
const ret = [Math.cos(Phi) * Math.cos(Theta), Math.cos(Phi) * Math.sin(Theta), Math.sin(Phi)];
const len = Math.sqrt(ret[0] * ret[0] + ret[1] * ret[1] + ret[2] * ret[2]);
if (len <= 0) {
return ret;
}
ret[0] /= len;
ret[1] /= len;
ret[2] /= len;
return ret;
}
static vox2mm(XYZ, mtx) {
const sform = mat4.clone(mtx);
mat4.transpose(sform, sform);
const pos = vec4.fromValues(XYZ[0], XYZ[1], XYZ[2], 1);
vec4.transformMat4(pos, pos, sform);
const pos3 = vec3.fromValues(pos[0], pos[1], pos[2]);
return pos3;
}
};
// src/utils/image-utils.ts
function img2ras16(volume) {
const dims = volume.hdr.dims;
const perm = volume.permRAS;
const vx = dims[1] * dims[2] * dims[3];
const img16 = new Int16Array(vx);
const layout = [0, 0, 0];
for (let i = 0; i < 3; i++) {
for (let j2 = 0; j2 < 3; j2++) {
if (Math.abs(perm[i]) - 1 !== j2) {
continue;
}
layout[j2] = i * Math.sign(perm[i]);
}
}
let stride = 1;
const instride = [1, 1, 1];
const inflip = [false, false, false];
for (let i = 0; i < layout.length; i++) {
for (let j2 = 0; j2 < layout.length; j2++) {
const a = Math.abs(layout[j2]);
if (a !== i) {
continue;
}
instride[j2] = stride;
if (layout[j2] < 0 || Object.is(layout[j2], -0)) {
inflip[j2] = true;
}
stride *= dims[j2 + 1];
}
}
let xlut = NVUtilities.range(0, dims[1] - 1, 1);
if (inflip[0]) {
xlut = NVUtilities.range(dims[1] - 1, 0, -1);
}
for (let i = 0; i < dims[1]; i++) {
xlut[i] *= instride[0];
}
let ylut = NVUtilities.range(0, dims[2] - 1, 1);
if (inflip[1]) {
ylut = NVUtilities.range(dims[2] - 1, 0, -1);
}
for (let i = 0; i < dims[2]; i++) {
ylut[i] *= instride[1];
}
let zlut = NVUtilities.range(0, dims[3] - 1, 1);
if (inflip[2]) {
zlut = NVUtilities.range(dims[3] - 1, 0, -1);
}
for (let i = 0; i < dims[3]; i++) {
zlut[i] *= instride[2];
}
let j = 0;
for (let z = 0; z < dims[3]; z++) {
for (let y = 0; y < dims[2]; y++) {
for (let x = 0; x < dims[1]; x++) {
img16[xlut[x] + ylut[y] + zlut[z]] = volume.img[j];
j++;
}
}
}
return img16;
}
function unpackFloatFromVec4i(val) {
const bitSh = [1 / (256 * 256 * 256), 1 / (256 * 256), 1 / 256, 1];
return (val[0] * bitSh[0] + val[1] * bitSh[1] + val[2] * bitSh[2] + val[3] * bitSh[3]) / 255;
}
function intensityRaw2Scaled(hdr, raw) {
if (hdr.scl_slope === 0) {
hdr.scl_slope = 1;
}
return raw * hdr.scl_slope + hdr.scl_inter;
}
// src/utils/math-utils.ts
function loose_label(min, max, ntick = 4) {
const range = nice(max - min, false);
const d = nice(range / (ntick - 1), true);
const graphmin = Math.floor(min / d) * d;
const graphmax = Math.ceil(max / d) * d;
const perfect = graphmin === min && graphmax === max;
return [d, graphmin, graphmax, perfect];
}
function tickSpacing(mn, mx) {
let v = loose_label(mn, mx, 3);
if (!v[3]) {
v = loose_label(mn, mx, 5);
}
if (!v[3]) {
v = loose_label(mn, mx, 4);
}
if (!v[3]) {
v = loose_label(mn, mx, 3);
}
if (!v[3]) {
v = loose_label(mn, mx, 5);
}
return [v[0], v[1], v[2]];
}
function deg2rad(deg) {
return deg * (Math.PI / 180);
}
function negMinMax(min, max, minNeg, maxNeg) {
let mn = -min;
let mx = -max;
if (isFinite(minNeg) && isFinite(maxNeg)) {
mn = minNeg;
mx = maxNeg;
}
if (mn > mx) {
;
[mn, mx] = [mx, mn];
}
return [mn, mx];
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// src/utils/webgl-utils.ts
import { mat4 as mat42, vec3 as vec32, vec4 as vec42 } from "gl-matrix";
function swizzleVec3(vec, order = [0, 1, 2]) {
const vout = vec32.create();
vout[0] = vec[order[0]];
vout[1] = vec[order[1]];
vout[2] = vec[order[2]];
return vout;
}
function isRadiological(mtx) {
const vRight = vec42.fromValues(1, 0, 0, 0);
const vRotated = vec42.create();
vec42.transformMat4(vRotated, vRight, mtx);
return vRotated[0];
}
function unProject(winX, winY, winZ, mvpMatrix) {
const inp = vec42.fromValues(winX, winY, winZ, 1);
const finalMatrix = mat42.clone(mvpMatrix);
mat42.invert(finalMatrix, finalMatrix);
inp[0] = inp[0] * 2 - 1;
inp[1] = inp[1] * 2 - 1;
inp[2] = inp[2] * 2 - 1;
const out = vec42.create();
vec42.transformMat4(out, inp, finalMatrix);
if (out[3] === 0) {
return out;
}
out[0] /= out[3];
out[1] /= out[3];
out[2] /= out[3];
return out;
}
export {
clamp,
deg2rad,
img2ras16,
intensityRaw2Scaled,
isRadiological,
negMinMax,
nice,
readFileAsDataURL,
swizzleVec3,
tickSpacing,
unProject,
unpackFloatFromVec4i
};
//# sourceMappingURL=index.js.map