All files / dicomImageLoader/src/imageLoader/wadouri dataset-from-partial-content.ts

0% Statements 0/31
0% Branches 0/4
0% Functions 0/4
0% Lines 0/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129                                                                                                                                                                                                                                                                 
import type { DataSet } from 'dicom-parser';
import * as dicomParser from 'dicom-parser';
import type {
  LoadRequestFunction,
  DICOMLoaderDataSetWithFetchMore,
} from '../../types';
 
function fixFragments(dataSet: DataSet) {
  // The partially parsed pixelData element has incorrect fragment
  // lengths because the byte array is truncated, so we manually set
  // it to the actual length.
  const fragments = dataSet.elements.x7fe00010.fragments;
  const totalLength = dataSet.byteArray.length;
 
  for (const fragment of fragments) {
    const { position, length } = fragment;
 
    if (length > totalLength - position) {
      console.log(
        `Truncated fragment, changing fragment length from ${
          fragment.length
        } to ${totalLength - position}`
      );
      fragment.length = totalLength - position;
    }
  }
 
  return dataSet;
}
 
function parsePartialByteArray(byteArray: Uint8Array) {
  /**
   * First parse just up to pixelData. This will make sure the
   * metadata header is correctly parsed (assuming no other error is
   * thrown during parsing). Then, parse again using the whole partial
   * arraybuffer. This will error, but still kick out the parsed
   * partial pixel data in the error object.
   */
 
  let dataSet = dicomParser.parseDicom(byteArray, {
    untilTag: 'x7fe00010',
  });
 
  if (!dataSet.elements.x7fe00010) {
    console.warn('Pixel data not found!');
    // Re-fetch more of the file
  }
 
  let pixelDataSet: DataSet;
 
  try {
    // This is expected to fail, since the file is incomplete, but
    // dicomParser helpfully spits out the parsed partial dataset in
    // the error object. The problem is, the dataset here is
    // incomplete, because dicomParser throws *before* combining the
    // metadata header and regular datasets, so transfer syntax and
    // other metadata headers aren't included.
    pixelDataSet = dicomParser.parseDicom(byteArray);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  } catch (err: any) {
    // Todo: This is probably invalid handling - it expects the only reason to
    //  fail is a partial dataset
    console.error(err);
    console.log('pixel data dataset:', err.dataSet);
    pixelDataSet = err.dataSet;
  }
 
  // Add the parsed partial pixel data element to the dataset
  // including the metadata headers.
  dataSet.elements.x7fe00010 = pixelDataSet.elements.x7fe00010;
 
  dataSet = fixFragments(dataSet);
 
  return dataSet;
}
 
export default async function dataSetFromPartialContent(
  byteArray: Uint8Array,
  loadRequest: LoadRequestFunction,
  metadata: {
    uri: string;
    imageId: string;
    fileTotalLength: number | null;
  }
): Promise<DICOMLoaderDataSetWithFetchMore> {
  const dataSet: DICOMLoaderDataSetWithFetchMore =
    parsePartialByteArray(byteArray);
  const { uri, imageId, fileTotalLength } = metadata;
 
  // Allow re-fetching of more of the file
  dataSet.fetchMore = async function (fetchOptions) {
    // Default to fetching the rest of the file if no lengthToFetch is set. Also
    // default to fetching the same URI/imageId
    const _options = Object.assign(
      {
        uri,
        imageId,
        fetchedLength: byteArray.length, // Not sure if this would ever need to be configurable tbh
        lengthToFetch: fileTotalLength - byteArray.length,
      },
      fetchOptions
    );
    const { fetchedLength, lengthToFetch } = _options;
 
    // Use passed xhr loader to re-fetch new byte range
 
    // Todo: the following might be wrong, does it return array buffer or
    // something else?
    // @ts-ignore
    const { arrayBuffer } = await loadRequest(uri, imageId, {
      byteRange: `${fetchedLength}-${fetchedLength + lengthToFetch}`,
    });
 
    // Combine byte ranges
    const byteArrayToAppend = new Uint8Array(arrayBuffer);
    const combinedByteArray = new Uint8Array(
      dataSet.byteArray.length + byteArrayToAppend.length
    );
 
    combinedByteArray.set(dataSet.byteArray);
    combinedByteArray.set(byteArrayToAppend, dataSet.byteArray.length);
 
    // Re-parse potentially partial byte range and return
    return dataSetFromPartialContent(combinedByteArray, loadRequest, metadata);
  };
 
  return dataSet;
}