import { NIFTI1 } from 'nifti-reader-js' import type { NVImage } from '@/nvimage' import { NiiDataType } from '@/nvimage/utils' /** * Helper function to convert ZXY RGB data to XYZ format for Zarr arrays. * @param data - Input data in ZXY order * @param X - Width dimension * @param Y - Height dimension * @param Z - Depth dimension * @returns Uint8Array in XYZ order */ function zxy2xyz(data: any, X: number, Y: number, Z: number): Uint8Array { const voxelCount = X * Y const rgb = new Uint8Array(voxelCount * Z * 3) const offsets = new Array(Z) for (let s = 0; s < Z; s++) { offsets[s] = voxelCount * 3 * s } let srcIndex = 0 let dstIndex = 0 for (let v = 0; v < voxelCount; v++) { for (let s = 0; s < Z; s++) { rgb[offsets[s] + dstIndex] = data[srcIndex++] // R rgb[offsets[s] + dstIndex + 1] = data[srcIndex++] // G rgb[offsets[s] + dstIndex + 2] = data[srcIndex++] // B } dstIndex += 3 } return rgb } /** * Reads Zarr multi-dimensional array format, modifying the provided * NVImage header and returning the raw image data buffer. * * Format specification: https://zarrita.dev/get-started.html * * Note: RGB channels may be returned as depth dimension. * * @param nvImage - The NVImage instance whose header will be modified. * @param buffer - ArrayBuffer containing the Zarr file data (unused, kept for consistency). * @param zarrData - Parsed Zarr data object containing width, height, depth, and data. * @returns Promise resolving to ArrayBuffer containing the image data. * @throws Error if data dimensions don't match expected size. */ export async function readZARR(nvImage: NVImage, buffer: ArrayBuffer, zarrData: unknown): Promise { let { width, height, depth = 1, data } = (zarrData ?? {}) as any let expectedLength = width * height * depth * 3 let isRGB = expectedLength === data.length if (!isRGB) { expectedLength = width * height * depth if (depth === 3) { // see https://zarrita.dev/get-started.html R,G,B channels returns as depth! isRGB = true depth = 1 } } if (expectedLength !== data.length) { throw new Error(`Expected RGB ${width}×${height}×${depth}×3 = ${expectedLength}, but ZARR length ${data.length}`) } nvImage.hdr = new NIFTI1() const hdr = nvImage.hdr hdr.dims = [3, width, height, depth, 1, 1, 1, 1] hdr.pixDims = [1, 1, 1, 1, 0, 0, 0, 0] hdr.affine = [ [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]], [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]], [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]], [0, 0, 0, 1] ] if (!isRGB) { hdr.numBitsPerVoxel = 8 hdr.datatypeCode = NiiDataType.DT_UINT8 // if data is a Uint8Array, convert to ArrayBuffer if (data instanceof Uint8Array) { const retBuffer = new ArrayBuffer(data.length) const retView = new Uint8Array(retBuffer) retView.set(data) return retBuffer } return data } hdr.numBitsPerVoxel = 24 hdr.datatypeCode = NiiDataType.DT_RGB24 const retData = zxy2xyz(data, hdr.dims[1], hdr.dims[2], hdr.dims[3]) // convert retData Uint8Array to ArrayBuffer const retBuffer = new ArrayBuffer(retData.length) const retView = new Uint8Array(retBuffer) retView.set(retData) return retBuffer }