UNPKG

dicom-microscopy-viewer-changed

Version:
721 lines (606 loc) 24.7 kB
import "regenerator-runtime/runtime.js"; import { getZippedTestDataset, getTestDataset } from "./testUtils.js"; import { jest } from "@jest/globals"; import dcmjs from "../src/index.js"; import { WriteBufferStream } from "../src/BufferStream"; import fs from "fs"; import fsPromises from "fs/promises"; import os from "os"; import path from "path"; import datasetWithNullNumberVRs from "./mocks/null_number_vrs_dataset.json"; import minimalDataset from "./mocks/minimal_fields_dataset.json"; import arrayItem from "./arrayItem.json"; import { rawTags } from "./rawTags"; import { promisify } from "util"; const { DicomMetaDictionary, DicomDict, DicomMessage, ReadBufferStream } = dcmjs.data; const IMPLICIT_LITTLE_ENDIAN = "1.2.840.10008.1.2"; const EXPLICIT_LITTLE_ENDIAN = "1.2.840.10008.1.2.1"; const fileMetaInformationVersionArray = new Uint8Array(2); fileMetaInformationVersionArray[1] = 1; // The asset downloads in this file might take some time on a slower connection jest.setTimeout(60000); const metadata = { "00020001": { Value: [fileMetaInformationVersionArray.buffer], vr: "OB" }, "00020012": { Value: ["1.2.840.113819.7.1.1997.1.0"], vr: "UI" }, "00020002": { Value: ["1.2.840.10008.5.1.4.1.1.4"], vr: "UI" }, "00020003": { Value: [DicomMetaDictionary.uid()], vr: "UI" }, "00020010": { Value: ["1.2.840.10008.1.2"], vr: "UI" } }; const sequenceMetadata = { "00080081": { vr: "ST", Value: [null] }, "00081032": { vr: "SQ", Value: [ { "00080100": { vr: "SH", Value: ["IMG1332"] }, "00080102": { vr: "SH", Value: ["L"] }, "00080104": { vr: "LO", Value: ["MRI SHOULDER WITHOUT IV CONTRAST LEFT"] } } ] }, 52009229: { vr: "SQ", Value: [ { "00289110": { vr: "SQ", Value: [ { "00180088": { vr: "DS", Value: [0.12] } } ] } } ] } }; function makeOverlayBitmap({ width, height }) { const topBottom = new Array(width).fill(1, 0, width); const middle = new Array(width).fill(0, 0, width); const bitmap = []; middle[0] = 1; middle[width - 1] = 1; bitmap.push(topBottom); for (let i = 0; i < height - 2; i++) { bitmap.push(middle); } bitmap.push(topBottom); return bitmap.flat(); } it("test_array_items", () => { const dicomJSON = JSON.stringify(arrayItem); const datasets = JSON.parse(dicomJSON); const natural0 = DicomMetaDictionary.naturalizeDataset(datasets[0]); // Shouldn't throw an exception const natural0b = DicomMetaDictionary.naturalizeDataset(datasets[0]); // And should be identical to the previous version expect(natural0b).toEqual(natural0); }); it("test_json_1", () => { // // multiple results example // from http://dicom.nema.org/medical/dicom/current/output/html/part18.html#chapter_F // const dicomJSON = ` [ { "0020000D": { "vr": "UI", "Value": [ "1.2.392.200036.9116.2.2.2.1762893313.1029997326.945873" ] } }, { "0020000D" : { "vr": "UI", "Value": [ "1.2.392.200036.9116.2.2.2.2162893313.1029997326.945876" ] } } ] `; const datasets = JSON.parse(dicomJSON); const firstUID = datasets[0]["0020000D"].Value[0]; const secondUID = datasets[1]["0020000D"].Value[0]; // // make a natural version of the first study and confirm it has correct value // const naturalDICOM = DicomMetaDictionary.naturalizeDataset(datasets[0]); expect(naturalDICOM.StudyInstanceUID).toEqual(firstUID); // // make a natural version of a dataset with sequence tags and confirm it has correct values // const naturalSequence = DicomMetaDictionary.naturalizeDataset( sequenceMetadata ); // The match object needs to be done on the actual element, not the proxied value expect(naturalSequence.ProcedureCodeSequence[0]).toMatchObject({ CodeValue: "IMG1332" }); // tests that single element sequences have been converted // from arrays to values. // See discussion here for more details: https://github.com/dcmjs-org/dcmjs/commit/74571a4bd6c793af2a679a31cec7e197f93e28cc const spacing = naturalSequence.SharedFunctionalGroupsSequence.PixelMeasuresSequence .SpacingBetweenSlices; expect(spacing).toEqual(0.12); expect( Array.isArray(naturalSequence.SharedFunctionalGroupsSequence) ).toEqual(true); expect(naturalSequence.ProcedureCodeSequence[0]).toMatchObject({ CodingSchemeDesignator: "L", CodeMeaning: "MRI SHOULDER WITHOUT IV CONTRAST LEFT" }); // expect original data to remain unnaturalized expect(sequenceMetadata["00081032"].Value[0]).toHaveProperty("00080100"); expect(sequenceMetadata["00081032"].Value[0]).toHaveProperty("00080102"); expect(sequenceMetadata["00081032"].Value[0]).toHaveProperty("00080104"); // // convert to part10 and back // const dicomDict = new DicomDict(metadata); dicomDict.dict = datasets[1]; const part10Buffer = dicomDict.write(); const dicomData = dcmjs.data.DicomMessage.readFile(part10Buffer); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomData.dict ); expect(dataset.StudyInstanceUID).toEqual(secondUID); }); it("test_multiframe_1", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/MRHead/MRHead.zip"; const unzipPath = await getZippedTestDataset(url, "MRHead.zip", "test_multiframe_1"); const mrHeadPath = path.join(unzipPath, "MRHead"); const fileNames = await fsPromises.readdir(mrHeadPath); const datasets = []; fileNames.forEach(fileName => { const arrayBuffer = fs.readFileSync(path.join(mrHeadPath, fileName)) .buffer; const dicomDict = DicomMessage.readFile(arrayBuffer); const dataset = DicomMetaDictionary.naturalizeDataset(dicomDict.dict); datasets.push(dataset); }); const multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset( datasets ); const spacing = multiframe.SharedFunctionalGroupsSequence.PixelMeasuresSequence .SpacingBetweenSlices; const roundedSpacing = Math.round(100 * spacing) / 100; expect(multiframe.NumberOfFrames).toEqual(130); expect(roundedSpacing).toEqual(1.3); }); it("test_oneslice_seg", async () => { const ctPelvisURL = "https://github.com/dcmjs-org/data/releases/download/CTPelvis/CTPelvis.zip"; const segURL = "https://github.com/dcmjs-org/data/releases/download/CTPelvis/Lesion1_onesliceSEG.dcm"; const unzipPath = await getZippedTestDataset(ctPelvisURL, "CTPelvis.zip", "test_oneslice_seg"); const segFileName = "Lesion1_onesliceSEG.dcm" const ctPelvisPath = path.join( unzipPath, "Series-1.2.840.113704.1.111.1916.1223562191.15" ); const fileNames = await fsPromises.readdir(ctPelvisPath); const datasets = []; fileNames.forEach(fileName => { const arrayBuffer = fs.readFileSync(path.join(ctPelvisPath, fileName)) .buffer; const dicomDict = DicomMessage.readFile(arrayBuffer); const dataset = DicomMetaDictionary.naturalizeDataset(dicomDict.dict); datasets.push(dataset); }); let multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset(datasets); const spacing = multiframe.SharedFunctionalGroupsSequence.PixelMeasuresSequence .SpacingBetweenSlices; const roundedSpacing = Math.round(100 * spacing) / 100; expect(multiframe.NumberOfFrames).toEqual(60); expect(roundedSpacing).toEqual(5); var segFilePath = await getTestDataset(segURL, segFileName); const arrayBuffer = fs.readFileSync(segFilePath).buffer; const dicomDict = DicomMessage.readFile(arrayBuffer); const dataset = DicomMetaDictionary.naturalizeDataset(dicomDict.dict); multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset([dataset]); expect(dataset.NumberOfFrames).toEqual(1); expect(multiframe.NumberOfFrames).toEqual(1); }); it("test_normalizer_smaller", () => { const naturalizedTags = dcmjs.data.DicomMetaDictionary.naturalizeDataset( rawTags ); const rawTagsLen = JSON.stringify(rawTags).length; const naturalizedTagsLen = JSON.stringify(naturalizedTags).length; expect(naturalizedTagsLen).toBeLessThan(rawTagsLen); }); it("test_multiframe_us", () => { const file = fs.readFileSync("test/cine-test.dcm"); const dicomData = dcmjs.data.DicomMessage.readFile(file.buffer, { // ignoreErrors: true, }); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomData.dict ); // eslint-disable-next-line no-underscore-dangle dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset( dicomData.meta ); expect(dataset.NumberOfFrames).toEqual(8); }); it("test_fragment_multiframe", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/encapsulation/encapsulation-fragment-multiframe.dcm"; const dcmPath = await getTestDataset(url, "encapsulation-fragment-multiframe.dcm") const file = fs.readFileSync(dcmPath); const dicomData = dcmjs.data.DicomMessage.readFile(file.buffer, { // ignoreErrors: true, }); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomData.dict ); // eslint-disable-next-line no-underscore-dangle dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset( dicomData.meta ); expect(dataset.NumberOfFrames).toEqual(2); }); it("test_null_number_vrs", () => { const dicomDict = new DicomDict({ TransferSynxtaxUID: "1.2.840.10008.1.2.1" }); dicomDict.dict = DicomMetaDictionary.denaturalizeDataset( datasetWithNullNumberVRs ); const part10Buffer = dicomDict.write(); const dicomData = dcmjs.data.DicomMessage.readFile(part10Buffer); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomData.dict ); expect(dataset.ImageAndFluoroscopyAreaDoseProduct).toEqual(null); expect(dataset.InstanceNumber).toEqual(null); }); it("test_exponential_notation", () => { const file = fs.readFileSync("test/sample-dicom.dcm"); const data = dcmjs.data.DicomMessage.readFile(file.buffer, { // ignoreErrors: true, }); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(data.dict); dataset.ImagePositionPatient[2] = 7.1945578383e-5; const buffer = data.write(); const copy = dcmjs.data.DicomMessage.readFile(buffer); expect(JSON.stringify(data)).toEqual(JSON.stringify(copy)); }); it("test_output_equality", () => { const file = fs.readFileSync("test/cine-test.dcm"); const dicomData1 = dcmjs.data.DicomMessage.readFile(file.buffer, { // ignoreErrors: true, }); const buffer = dicomData1.write(); const dicomData2 = dcmjs.data.DicomMessage.readFile(buffer, { // ignoreErrors: true, }); check_equality(dicomData1.meta, dicomData2.meta); check_equality(dicomData1.dict, dicomData2.dict); function check_equality(dict1, dict2) { Object.keys(dict1).forEach(key => { const elem1 = dict1[key]; const elem2 = dict2[key]; expect(JSON.stringify(elem1)).toEqual(JSON.stringify(elem2)); }); } }); it("test_performance", async () => { const file = fs.readFileSync("test/cine-test.dcm"); let buffer = file.buffer; let json; const start = Date.now(); for (let i = 0; i < 100; ++i) { let old = json; json = DicomMessage.readFile(buffer); buffer = json.write(); if (i > 0) { check_equality(old.meta, json.meta); check_equality(old.dict, json.dict); } } function check_equality(dict1, dict2) { Object.keys(dict1).forEach(key => { const elem1 = dict1[key]; const elem2 = dict2[key]; expect(JSON.stringify(elem1)).toEqual(JSON.stringify(elem2)); }); } console.log(`Finished. Total Time elapsed: ${Date.now() - start} ms`); }); it("test_invalid_vr_length", () => { const file = fs.readFileSync("test/invalid-vr-length-test.dcm"); const dicomDict = dcmjs.data.DicomMessage.readFile(file.buffer); expect(() => writeToBuffer(dicomDict, { allowInvalidVRLength: false }) ).toThrow(); expect(() => writeToBuffer(dicomDict, { allowInvalidVRLength: true }) ).not.toThrow(); function writeToBuffer(dicomDict, options) { return dicomDict.write(options); } }); it("test_encapsulation", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/encapsulation/encapsulation.dcm"; const dcmPath = await getTestDataset(url, "encapsulation.dcm") // given const arrayBuffer = fs.readFileSync(dcmPath).buffer; const dicomDict = DicomMessage.readFile(arrayBuffer); dicomDict.upsertTag("60000010", "US", 30); // Overlay Rows dicomDict.upsertTag("60000011", "US", 30); // Overlay Columns dicomDict.upsertTag("60000040", "CS", "G"); // Overlay Type dicomDict.upsertTag("60000045", "LO", "AUTOMATED"); // Overlay Subtype dicomDict.upsertTag("60000050", "SS", [1 + 50, 1 + 50]); // Overlay Origin let overlay = dcmjs.data.BitArray.pack( makeOverlayBitmap({ width: 30, height: 30 }) ); if (overlay.length % 2 !== 0) { const newOverlay = new Uint8Array(overlay.length + 1); newOverlay.set(overlay); newOverlay.set([0], overlay.length); overlay = newOverlay; } dicomDict.upsertTag("60003000", "OB", [overlay.buffer]); // when const lengths = []; const stream = new ReadBufferStream( dicomDict.write({ fragmentMultiframe: false }) ), useSyntax = EXPLICIT_LITTLE_ENDIAN; stream.reset(); stream.increment(128); if (stream.readAsciiString(4) !== "DICM") { throw new Error("Invalid a dicom file"); } const el = DicomMessage._readTag(stream, useSyntax), metaLength = el.values[0]; //read header buffer const metaStream = stream.more(metaLength); const metaHeader = DicomMessage._read(metaStream, useSyntax); //get the syntax let mainSyntax = metaHeader["00020010"].Value[0]; mainSyntax = DicomMessage._normalizeSyntax(mainSyntax); while (!stream.end()) { const group = new Uint16Array(stream.buffer, stream.offset, 1)[0] .toString(16) .padStart(4, "0"); const element = new Uint16Array(stream.buffer, stream.offset + 2, 1)[0] .toString(16) .padStart(4, "0"); if (group.concat(element) === "60003000") { // Overlay Data const length = Buffer.from( new Uint8Array(stream.buffer, stream.offset + 8, 4) ).readUInt32LE(0); lengths.push(length); } if (group.concat(element) === "7fe00010") { // Pixel Data const length = Buffer.from( new Uint8Array(stream.buffer, stream.offset + 8, 4) ).readUInt32LE(0); lengths.push(length); } DicomMessage._readTag(stream, mainSyntax); } // then expect(lengths[0]).not.toEqual(0xffffffff); expect(lengths[1]).toEqual(0xffffffff); }); it("test_custom_dictionary", () => { const customDictionary = DicomMetaDictionary.dictionary; customDictionary["(0013,1010)"] = { tag: "(0013,1010)", vr: "LO", name: "TrialName", vm: "1", version: "Custom" }; const dicomMetaDictionary = new DicomMetaDictionary(customDictionary); const dicomDict = new DicomDict(metadata); minimalDataset["TrialName"] = "Test Trial"; dicomDict.dict = dicomMetaDictionary.denaturalizeDataset(minimalDataset); const part10Buffer = dicomDict.write(); const dicomData = DicomMessage.readFile(part10Buffer); const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); expect(dataset.TrialName).toEqual("Test Trial"); //check that all other fields were preserved, 15 original + 1 for _vr and +1 for "TrialName" expect(Object.keys(dataset).length).toEqual(17); }); it("Reads DICOM with multiplicity", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/multiplicity/multiplicity.dcm"; const dcmPath = await getTestDataset(url, "multiplicity.dcm") const file = await promisify(fs.readFile)(dcmPath); const dicomDict = DicomMessage.readFile(file.buffer); expect(dicomDict.dict["00101020"].Value).toEqual([1, 2]); expect(dicomDict.dict["0018100B"].Value).toEqual(["1.2", "3.4"]); }); it("Reads binary data into an ArrayBuffer", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/binary-tag/binary-tag.dcm"; const dcmPath = await getTestDataset(url, "binary-tag.dcm") const file = await promisify(fs.readFile)(dcmPath); const dicomDict = DicomMessage.readFile(file.buffer); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomDict.dict ); expect(dataset.PixelData).toBeInstanceOf(Array); expect(dataset.PixelData[0]).toBeInstanceOf(ArrayBuffer); expect([...new Uint8Array(dataset.PixelData[0])]).toEqual([2, 3, 4, 5, 6]); }); it("Reads a multiframe DICOM which has trailing padding", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/binary-parsing-stressors/multiframe-ultrasound.dcm"; const dcmPath = await getTestDataset(url, "multiframe-ultrasound.dcm") const dicomDict = DicomMessage.readFile(fs.readFileSync(dcmPath).buffer); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomDict.dict ); expect(dataset.PixelData.length).toEqual(29); expect(dataset.PixelData[0]).toBeInstanceOf(ArrayBuffer); expect(dataset.PixelData[0].byteLength).toEqual(104976); expect(dataset.PixelData[1].byteLength).toEqual(104920); expect(dataset.PixelData[27].byteLength).toEqual(103168); expect(dataset.PixelData[28].byteLength).toEqual(103194); }); it("Reads a multiframe DICOM with large private tags before and after the image data", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/binary-parsing-stressors/large-private-tags.dcm"; const dcmPath = await getTestDataset(url, "large-private-tags.dcm") const dicomDict = DicomMessage.readFile(fs.readFileSync(dcmPath).buffer); const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomDict.dict ); expect(dataset.PixelData).toBeInstanceOf(Array); expect(dataset.PixelData.length).toEqual(130); expect(dataset.PixelData[0]).toBeInstanceOf(ArrayBuffer); expect(dataset.PixelData[0].byteLength).toEqual(61518); expect(dataset.PixelData[1].byteLength).toEqual(61482); expect(dataset.PixelData[128].byteLength).toEqual(62144); expect(dataset.PixelData[129].byteLength).toEqual(62148); }); it("Writes encapsulated OB data which has an odd length with a padding byte in its last fragment", async () => { const pixelData = [1, 2, 3]; const dataset = DicomMetaDictionary.denaturalizeDataset({ PixelData: [new Uint8Array(pixelData).buffer], _vrMap: { PixelData: "OB" } }); const stream = new WriteBufferStream(1024); const bytesWritten = DicomMessage.write( dataset, stream, "1.2.840.10008.1.2.4.50" // JPEG baseline (an encapsulated format) ); expect(bytesWritten).toEqual(44); expect([...new Uint32Array(stream.view.buffer, 0, 11)]).toEqual([ 0x00107fe0, // PixelData tag's group & element 0x0000424f, // VR type "OB" 0xffffffff, // Value length (0xffffffff here indicates an undefined length) 0xe000fffe, // SequenceItemTag for the BOT (basic offset table) 0x00000004, // Size in bytes of the BOT 0x00000000, // First (and only) offset in the BOT 0xe000fffe, // SequenceItemTag 0x00000004, // SequenceItemTag's length in bytes 0x00030201, // The actual data for this fragment (specified above), with padding 0xe0ddfffe, // SequenceDelimiterTag 0x00000000 // SequenceDelimiterTag value (always zero) ]); }); describe("With a SpecificCharacterSet tag", () => { it("Reads a long string in the '' character set", async () => { expect(readEncodedLongString("", [0x68, 0x69])).toEqual("hi"); }); it("Reads a long string in the ISO_IR 6 (default) character set", async () => { expect(readEncodedLongString("ISO_IR 6", [0x68, 0x69])).toEqual("hi"); }); it("Reads a long string in the ISO_IR 13 (shift-jis) character set", async () => { expect(readEncodedLongString("ISO_IR 13", [0x83, 0x8b])).toEqual("ル"); }); it("Reads a long string in the ISO_IR 166 (tis-620) character set", async () => { expect(readEncodedLongString("ISO_IR 166", [0xb9, 0xf7])).toEqual("น๗"); }); it("Reads a long string in the ISO_IR 192 (utf-8) character set", async () => { expect(readEncodedLongString("ISO_IR 192", [0xed, 0x95, 0x9c])).toEqual( "한" ); }); it("Throws an exception on an unsupported character set", async () => { expect(() => readEncodedLongString("nope", [])).toThrow( new Error("Unsupported character set: nope") ); }); it("Doesn't throw an exception on an unsupported character set when ignoring errors", async () => { expect( readEncodedLongString("nope", [0x68, 0x69], { ignoreErrors: true }) ).toEqual("hi"); }); it("Throws an exception on multiple character sets", async () => { expect(() => readEncodedLongString("ISO_IR 13\\ISO_IR 166", []) ).toThrow( /Using multiple character sets is not supported: ISO_IR 13,ISO_IR 166/ ); }); it("Doesn't throw an exception on multiple character sets when ignoring errors", async () => { expect( readEncodedLongString("ISO_IR 13\\ISO_IR 166", [0x68, 0x69], { ignoreErrors: true }) ).toEqual("hi"); }); function readEncodedLongString( specificCharacterSet, encodedBytes, readOptions = { ignoreErrors: false } ) { // Pad to even lengths with spaces if needed if (specificCharacterSet.length & 1) { specificCharacterSet += " "; } if (encodedBytes.length & 1) { encodedBytes.push(0x20); } // Manually construct the binary representation for the following two tags: // - Tag #1: SpecificCharacterSet specifying the character set // - Tag #2: InstitutionName which is a long string tag that will have its value // set to the encoded bytes const stream = new WriteBufferStream( 16 + specificCharacterSet.length + encodedBytes.length ); stream.isLittleEndian = true; // Write SpecificCharacterSet tag stream.writeUint32(0x00050008); stream.writeUint32(specificCharacterSet.length); stream.writeAsciiString(specificCharacterSet); // Write InstitutionName tag stream.writeUint32(0x00800008); stream.writeUint32(encodedBytes.length); for (const encodedByte of encodedBytes) { stream.writeUint8(encodedByte); } // Read the stream back to get the value of the InstitutionName tag const readResult = DicomMessage._read( new ReadBufferStream(stream.buffer), IMPLICIT_LITTLE_ENDIAN, readOptions ); // Return the resulting UTF-8 string value for InstitutionName return readResult["00080080"].Value[0]; } });