// Read a KomaMRI `.phantom` (HDF5) file into a Phantom, using h5wasm in the // browser. Robust to missing contrast fields (fills sensible defaults) so it // can also load arbitrary KomaMRI phantoms, not just our two built-ins. import * as h5wasm from 'h5wasm' import type { Phantom } from './phantomTypes.ts' let readyPromise: Promise | null = null let counter = 0 async function ensureReady(): Promise { if (!readyPromise) { readyPromise = h5wasm.ready.then(() => undefined) } return readyPromise } function toF32(v: unknown, ns: number, fallback: number): Float32Array { if (v instanceof Float32Array) return v if (v instanceof Float64Array || Array.isArray(v)) return Float32Array.from(v as ArrayLike) if (ArrayBuffer.isView(v)) return Float32Array.from(v as unknown as ArrayLike) const a = new Float32Array(ns) a.fill(fallback) return a } /** Map a contrast group's (possibly Unicode-named) datasets onto our field names. */ const CONTRAST_ALIASES: Record = { rho: ['ρ', 'rho', 'Rho', 'PD'], t1: ['T1'], t2: ['T2'], t2s: ['T2s', 'T2*'], dw: ['Δw', 'Deltaw', 'dw', 'B0'], } function readGroupField(group: h5wasm.Group | null, aliases: string[]): unknown { if (!group) return null const keys = group.keys() for (const alias of aliases) { if (keys.includes(alias)) { const ds = group.get(alias) if (ds && 'value' in ds) return (ds as h5wasm.Dataset).value } } return null } export async function loadPhantom(buffer: ArrayBuffer, fallbackName = 'phantom'): Promise { await ensureReady() const FS = h5wasm.FS! const filename = `/phantom_${counter++}.h5` FS.writeFile(filename, new Uint8Array(buffer)) let f: h5wasm.File | null = null try { f = new h5wasm.File(filename, 'r') const posGroup = f.get('position') as h5wasm.Group | null const x0 = readGroupField(posGroup, ['x']) const y0 = readGroupField(posGroup, ['y']) const z0 = readGroupField(posGroup, ['z']) const ns = x0 instanceof Float32Array || x0 instanceof Float64Array || Array.isArray(x0) ? (x0 as ArrayLike).length : 0 if (!ns) throw new Error('Phantom has no position/x dataset (not a valid .phantom file?)') const x = toF32(x0, ns, 0) const y = toF32(y0, ns, 0) const z = toF32(z0, ns, 0) const con = f.get('contrast') as h5wasm.Group | null const rho = toF32(readGroupField(con, CONTRAST_ALIASES.rho), ns, 1) const t1 = toF32(readGroupField(con, CONTRAST_ALIASES.t1), ns, 1) const t2 = toF32(readGroupField(con, CONTRAST_ALIASES.t2), ns, 0.1) const t2s = toF32(readGroupField(con, CONTRAST_ALIASES.t2s), ns, 0.05) const dw = toF32(readGroupField(con, CONTRAST_ALIASES.dw), ns, 0) const nameAttr = f.attrs['Name']?.value const name = typeof nameAttr === 'string' && nameAttr.length ? nameAttr : fallbackName return { name, ns, x, y, z, rho, t1, t2, t2s, dw } } finally { f?.close() try { FS.unlink(filename) } catch { // ignore } } }