// SPDX-License-Identifier: Apache-2.0 // Where the CT volume comes from: the public OME-Zarr scans in the Vesuvius Challenge open data bucket, // read straight from S3 over HTTPS (the bucket answers GET with Access-Control-Allow-Origin: *). // The global endpoint is used: the us-east-1 regional host answers 403 for the same keys from here. export const BUCKET = 'https://vesuvius-challenge-open-data.s3.amazonaws.com'; // scan volumes whose voxel grid the meshes of a scroll were traced on (tifxyz x, y, z = zarr x, y, z at level 0) export const VOLUMES = { PHerc0139: { zarr: `${BUCKET}/PHerc0139/volumes/20250728140407-9.362um-1.2m-113keV-masked.zarr`, voxelUM: 9.362 }, }; export function volumeFor(scroll, manifest) { const q = new URLSearchParams(location.search).get('ctzarr'); if (q && q.startsWith(BUCKET + '/')) return { zarr: q.replace(/\/$/, ''), voxelUM: +(new URLSearchParams(location.search).get('ctum')) || 9.362 }; if (manifest?.ctZarr?.startsWith?.(BUCKET + '/')) return { zarr: manifest.ctZarr, voxelUM: manifest.ctVoxelUM || 9.362 }; return VOLUMES[scroll] || null; } // level metadata (.zarray, zarr v2) const META = new Map(); export async function levelMeta(zarr, level) { const k = `${zarr}/${level}`; if (!META.has(k)) META.set(k, fetch(`${zarr}/${level}/.zarray`).then((r) => { if (!r.ok) throw new Error(`no level ${level} (${r.status})`); return r.json(); })); return META.get(k); } // Read the box [start, start + size) (z, y, x) of one level into a Uint8Array (C order, x fastest). // Simple path: uncompressed uint8 chunks fetched in parallel on the main thread; onChunk(box) reports each one. export async function readRegion(zarr, level, start, size, onChunk, signal) { const m = await levelMeta(zarr, level); if (m.dtype !== '|u1' || m.compressor) throw new Error(`level ${level}: ${m.dtype} ${m.compressor?.id || 'raw'} needs the streaming reader`); const [Z, Y, X] = size, out = new Uint8Array(Z * Y * X), C = m.chunks, sep = m.dimension_separator || '.'; const jobs = []; for (let cz = Math.floor(start[0] / C[0]); cz * C[0] < start[0] + Z; cz++) for (let cy = Math.floor(start[1] / C[1]); cy * C[1] < start[1] + Y; cy++) for (let cx = Math.floor(start[2] / C[2]); cx * C[2] < start[2] + X; cx++) jobs.push([cz, cy, cx]); let i = 0, n = 0; const one = async ([cz, cy, cx]) => { const r = await fetch(`${zarr}/${level}/${[cz, cy, cx].join(sep)}`, { signal }); const buf = r.ok ? new Uint8Array(await r.arrayBuffer()) : null; // a missing chunk is fill_value (0) const z0 = Math.max(start[0], cz * C[0]), z1 = Math.min(start[0] + Z, (cz + 1) * C[0]); const y0 = Math.max(start[1], cy * C[1]), y1 = Math.min(start[1] + Y, (cy + 1) * C[1]); const x0 = Math.max(start[2], cx * C[2]), x1 = Math.min(start[2] + X, (cx + 1) * C[2]); if (buf) for (let z = z0; z < z1; z++) for (let y = y0; y < y1; y++) { const src = ((z - cz * C[0]) * C[1] + (y - cy * C[1])) * C[2] + (x0 - cx * C[2]); out.set(buf.subarray(src, src + (x1 - x0)), ((z - start[0]) * Y + (y - start[1])) * X + (x0 - start[2])); } n++; onChunk?.({ done: n, total: jobs.length, bytes: buf ? buf.length : 0 }); }; await Promise.all(Array.from({ length: 6 }, async () => { while (i < jobs.length) await one(jobs[i++]); })); return { data: out, chunks: jobs.length }; }