diff --git a/src/DicomDict.js b/src/DicomDict.js index 1856e9e0..de57d467 100644 --- a/src/DicomDict.js +++ b/src/DicomDict.js @@ -17,7 +17,7 @@ class DicomDict { } } - write(writeOptions = {}) { + write(writeOptions = { allowInvalidVRLength: false }) { var metaSyntax = EXPLICIT_LITTLE_ENDIAN; var fileStream = new WriteBufferStream(4096, true); fileStream.writeHex("00".repeat(128)); @@ -36,7 +36,8 @@ class DicomDict { "00020000", "UL", metaStream.size, - metaSyntax + metaSyntax, + writeOptions ); fileStream.concat(metaStream); diff --git a/src/DicomMessage.js b/src/DicomMessage.js index ebf1256d..24962ebc 100644 --- a/src/DicomMessage.js +++ b/src/DicomMessage.js @@ -94,10 +94,10 @@ class DicomMessage { return dicomDict; } - static writeTagObject(stream, tagString, vr, values, syntax) { + static writeTagObject(stream, tagString, vr, values, syntax, writeOptions) { var tag = Tag.fromString(tagString); - tag.write(stream, vr, values, syntax); + tag.write(stream, vr, values, syntax, writeOptions); } static write(jsonObjects, useStream, syntax, writeOptions) { diff --git a/src/Tag.js b/src/Tag.js index d73a9b01..052b199e 100644 --- a/src/Tag.js +++ b/src/Tag.js @@ -116,13 +116,15 @@ class Tag { isEncapsulated, writeOptions ); - } else { + } else if (vrType == "SQ") { valueLength = vr.writeBytes( tagStream, values, useSyntax, writeOptions ); + } else { + valueLength = vr.writeBytes(tagStream, values, writeOptions); } if (vrType == "SQ") { diff --git a/src/ValueRepresentation.js b/src/ValueRepresentation.js index d5f50d7e..e53c1cfa 100644 --- a/src/ValueRepresentation.js +++ b/src/ValueRepresentation.js @@ -121,7 +121,13 @@ class ValueRepresentation { } } - writeBytes(stream, value, lengths) { + writeBytes( + stream, + value, + lengths, + writeOptions = { allowInvalidVRLength: false } + ) { + const { allowInvalidVRLength } = writeOptions; var valid = true, valarr = Array.isArray(value) ? value : [value], total = 0; @@ -131,7 +137,7 @@ class ValueRepresentation { checklen = lengths[i], isString = false, displaylen = checklen; - if (checkValue === null) { + if (checkValue === null || allowInvalidVRLength) { valid = true; } else if (this.checkLength) { valid = this.checkLength(checkValue); @@ -144,14 +150,14 @@ class ValueRepresentation { valid = checklen <= this.maxLength; } - var errmsg = - "Value exceeds max length, vr: " + - this.type + - ", value: " + - checkValue + - ", length: " + - displaylen; if (!valid) { + var errmsg = + "Value exceeds max length, vr: " + + this.type + + ", value: " + + checkValue + + ", length: " + + displaylen; if (isString) log.log(errmsg); else throw new Error(errmsg); } @@ -228,10 +234,12 @@ class StringRepresentation extends ValueRepresentation { return stream.readString(length); } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { + // TODO will delete + if (!writeOptions) throw new Error("writeOptions is undefined"); const written = super.write(stream, "String", value); - return super.writeBytes(stream, value, written); + return super.writeBytes(stream, value, written, writeOptions); } } @@ -321,7 +329,12 @@ class BinaryRepresentation extends ValueRepresentation { var binaryData = value[0]; binaryStream = new ReadBufferStream(binaryData); stream.concat(binaryStream); - return super.writeBytes(stream, binaryData, [binaryStream.size]); + return super.writeBytes( + stream, + binaryData, + [binaryStream.size], + writeOptions + ); } } @@ -455,11 +468,12 @@ class AttributeTag extends ValueRepresentation { return tagFromNumbers(group, element).value; } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { return super.writeBytes( stream, value, - super.write(stream, "TwoUint16s", value) + super.write(stream, "TwoUint16s", value), + writeOptions ); } } @@ -497,9 +511,9 @@ class DecimalString extends StringRepresentation { return ds; } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { const val = Array.isArray(value) ? value.map(String) : [value]; - return super.writeBytes(stream, val); + return super.writeBytes(stream, val, writeOptions); } } @@ -524,11 +538,12 @@ class FloatingPointSingle extends ValueRepresentation { return Number(stream.readFloat()); } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { return super.writeBytes( stream, value, - super.write(stream, "Float", value) + super.write(stream, "Float", value), + writeOptions ); } } @@ -546,11 +561,12 @@ class FloatingPointDouble extends ValueRepresentation { return Number(stream.readDouble()); } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { return super.writeBytes( stream, value, - super.write(stream, "Double", value) + super.write(stream, "Double", value), + writeOptions ); } } @@ -579,9 +595,9 @@ class IntegerString extends StringRepresentation { return is; } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { const val = Array.isArray(value) ? value.map(String) : [value]; - return super.writeBytes(stream, val); + return super.writeBytes(stream, val, writeOptions); } } @@ -672,11 +688,12 @@ class SignedLong extends ValueRepresentation { return stream.readInt32(); } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { return super.writeBytes( stream, value, - super.write(stream, "Int32", value) + super.write(stream, "Int32", value), + writeOptions ); } } @@ -806,7 +823,7 @@ class SequenceOfItems extends ValueRepresentation { super.write(stream, "Uint32", 0x00000000); written += 8; - return super.writeBytes(stream, value, [written]); + return super.writeBytes(stream, value, [written], writeOptions); } } @@ -824,11 +841,12 @@ class SignedShort extends ValueRepresentation { return stream.readInt16(); } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { return super.writeBytes( stream, value, - super.write(stream, "Int16", value) + super.write(stream, "Int16", value), + writeOptions ); } } @@ -897,11 +915,12 @@ class UnsignedShort extends ValueRepresentation { return stream.readUint16(); } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { return super.writeBytes( stream, value, - super.write(stream, "Uint16", value) + super.write(stream, "Uint16", value), + writeOptions ); } } @@ -919,11 +938,12 @@ class UnsignedLong extends ValueRepresentation { return stream.readUint32(); } - writeBytes(stream, value) { + writeBytes(stream, value, writeOptions) { return super.writeBytes( stream, value, - super.write(stream, "Uint32", value) + super.write(stream, "Uint32", value), + writeOptions ); } } diff --git a/test/invalid-vr-length-test.dcm b/test/invalid-vr-length-test.dcm new file mode 100644 index 00000000..365aad11 Binary files /dev/null and b/test/invalid-vr-length-test.dcm differ diff --git a/test/test_data.js b/test/test_data.js index 66739252..66dd2242 100644 --- a/test/test_data.js +++ b/test/test_data.js @@ -1,12 +1,12 @@ -const expect = require('chai').expect; -const dcmjs = require('../build/dcmjs'); +const expect = require("chai").expect; +const dcmjs = require("../build/dcmjs"); const fs = require("fs"); const { http, https } = require("follow-redirects"); const os = require("os"); const path = require("path"); const unzipper = require("unzipper"); -const datasetWithNullNumberVRs = require('./mocks/null_number_vrs_dataset.json'); +const datasetWithNullNumberVRs = require("./mocks/null_number_vrs_dataset.json"); const { DicomMetaDictionary, DicomDict, DicomMessage } = dcmjs.data; @@ -15,81 +15,65 @@ fileMetaInformationVersionArray[1] = 1; const metadata = { "00020001": { - "Value": [ - fileMetaInformationVersionArray.buffer - ], - "vr": "OB" + Value: [fileMetaInformationVersionArray.buffer], + vr: "OB" }, "00020012": { - "Value": [ - "1.2.840.113819.7.1.1997.1.0" - ], - "vr": "UI" + 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" + Value: ["1.2.840.10008.5.1.4.1.1.4"], + vr: "UI" }, "00020003": { - "Value": [ - DicomMetaDictionary.uid() - ], - "vr": "UI" + Value: [DicomMetaDictionary.uid()], + vr: "UI" }, "00020010": { - "Value": [ - "1.2.840.10008.1.2" - ], - "vr": "UI" + Value: ["1.2.840.10008.1.2"], + vr: "UI" } }; const sequenceMetadata = { "00081032": { - "vr": "SQ", - "Value": [ + vr: "SQ", + Value: [ { "00080100": { - "vr": "SH", - "Value": [ - "IMG1332" - ] + vr: "SH", + Value: ["IMG1332"] }, "00080102": { - "vr": "SH", - "Value": [ - "L" - ] + vr: "SH", + Value: ["L"] }, "00080104": { - "vr": "LO", - "Value": [ - "MRI SHOULDER WITHOUT IV CONTRAST LEFT" - ] + vr: "LO", + Value: ["MRI SHOULDER WITHOUT IV CONTRAST LEFT"] } } ] } -} +}; function downloadToFile(url, filePath) { return new Promise((resolve, reject) => { const fileStream = fs.createWriteStream(filePath); - const request = https.get(url, (response) => { - response.pipe(fileStream); - fileStream.on('finish', () => { - resolve(filePath); - }); - }).on('error', reject); + const request = https + .get(url, response => { + response.pipe(fileStream); + fileStream.on("finish", () => { + resolve(filePath); + }); + }) + .on("error", reject); }); } const tests = { - test_json_1: () => { - // // multiple results example // from http://dicom.nema.org/medical/dicom/current/output/html/part18.html#chapter_F @@ -111,8 +95,8 @@ const tests = { ] `; const datasets = JSON.parse(dicomJSON); - const firstUID = datasets[0]['0020000D'].Value[0]; - const secondUID = datasets[1]['0020000D'].Value[0]; + 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 @@ -124,13 +108,28 @@ const tests = { // // make a natural version of a dataset with sequence tags and confirm it has correct values // - const naturalSequence = DicomMetaDictionary.naturalizeDataset(sequenceMetadata); - - expect(naturalSequence.ProcedureCodeSequence).to.have.property('CodeValue', 'IMG1332'); - expect(naturalSequence.ProcedureCodeSequence).to.have.property('CodingSchemeDesignator', 'L'); - expect(naturalSequence.ProcedureCodeSequence).to.have.property('CodeMeaning', 'MRI SHOULDER WITHOUT IV CONTRAST LEFT'); + const naturalSequence = DicomMetaDictionary.naturalizeDataset( + sequenceMetadata + ); + + expect(naturalSequence.ProcedureCodeSequence).to.have.property( + "CodeValue", + "IMG1332" + ); + expect(naturalSequence.ProcedureCodeSequence).to.have.property( + "CodingSchemeDesignator", + "L" + ); + expect(naturalSequence.ProcedureCodeSequence).to.have.property( + "CodeMeaning", + "MRI SHOULDER WITHOUT IV CONTRAST LEFT" + ); // expect original data to remain unnaturalized - expect(sequenceMetadata['00081032'].Value[0]).to.have.keys('00080100', '00080102', '00080104'); + expect(sequenceMetadata["00081032"].Value[0]).to.have.keys( + "00080100", + "00080102", + "00080104" + ); // // convert to part10 and back @@ -140,111 +139,149 @@ const tests = { const part10Buffer = dicomDict.write(); const dicomData = dcmjs.data.DicomMessage.readFile(part10Buffer); - const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict); + const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( + dicomData.dict + ); expect(dataset.StudyInstanceUID).to.equal(secondUID); console.log("Finished test_json_1"); }, test_multiframe_1: () => { - - const url = "https://github.com/dcmjs-org/data/releases/download/MRHead/MRHead.zip"; + const url = + "https://github.com/dcmjs-org/data/releases/download/MRHead/MRHead.zip"; const zipFilePath = path.join(os.tmpdir(), "MRHead.zip"); const unzipPath = path.join(os.tmpdir(), "test_multiframe_1"); - downloadToFile(url, zipFilePath) - .then(() => { - fs.createReadStream(zipFilePath) - .pipe(unzipper.Extract({ path: unzipPath }) - .on('close', () => { - const mrHeadPath = path.join(unzipPath, "MRHead"); - fs.readdir(mrHeadPath, (err, fileNames) => { - expect(err).to.equal(null); - 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).to.equal(130); - expect(roundedSpacing).to.equal(1.3); - console.log("Finished test_multiframe_1"); - }) - }) - ); - }); + downloadToFile(url, zipFilePath).then(() => { + fs.createReadStream(zipFilePath).pipe( + unzipper.Extract({ path: unzipPath }).on("close", () => { + const mrHeadPath = path.join(unzipPath, "MRHead"); + fs.readdir(mrHeadPath, (err, fileNames) => { + expect(err).to.equal(null); + 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).to.equal(130); + expect(roundedSpacing).to.equal(1.3); + console.log("Finished test_multiframe_1"); + }); + }) + ); + }); }, test_oneslice_seg: () => { - - 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 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 zipFilePath = path.join(os.tmpdir(), "CTPelvis.zip"); const unzipPath = path.join(os.tmpdir(), "test_oneslice_seg"); const segFilePath = path.join(os.tmpdir(), "Lesion1_onesliceSEG.dcm"); - downloadToFile(ctPelvisURL, zipFilePath) - .then(() => { - fs.createReadStream(zipFilePath) - .pipe(unzipper.Extract({ path: unzipPath }) - .on('close', () => { - const ctPelvisPath = path.join(unzipPath, "Series-1.2.840.113704.1.111.1916.1223562191.15"); - fs.readdir(ctPelvisPath, (err, fileNames) => { - expect(err).to.equal(null); - 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); - }); - - const multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset(datasets); - const spacing = multiframe.SharedFunctionalGroupsSequence.PixelMeasuresSequence.SpacingBetweenSlices; - const roundedSpacing = Math.round(100 * spacing) / 100; - - expect(multiframe.NumberOfFrames).to.equal(60); - expect(roundedSpacing).to.equal(5); - - downloadToFile(segURL, segFilePath) - .then(() => { - const arrayBuffer = fs.readFileSync(segFilePath).buffer; - const dicomDict = DicomMessage.readFile(arrayBuffer); - const dataset = DicomMetaDictionary.naturalizeDataset(dicomDict.dict); - const multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset([dataset]); - expect(dataset.NumberOfFrames).to.equal(1); - expect(multiframe.NumberOfFrames).to.equal(1); - console.log("Finished test_oneslice_seg"); - }); - }) - }) + downloadToFile(ctPelvisURL, zipFilePath).then(() => { + fs.createReadStream(zipFilePath).pipe( + unzipper.Extract({ path: unzipPath }).on("close", () => { + const ctPelvisPath = path.join( + unzipPath, + "Series-1.2.840.113704.1.111.1916.1223562191.15" ); - }); + fs.readdir(ctPelvisPath, (err, fileNames) => { + expect(err).to.equal(null); + 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); + }); + + const multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset( + datasets + ); + const spacing = + multiframe.SharedFunctionalGroupsSequence + .PixelMeasuresSequence.SpacingBetweenSlices; + const roundedSpacing = Math.round(100 * spacing) / 100; + + expect(multiframe.NumberOfFrames).to.equal(60); + expect(roundedSpacing).to.equal(5); + + downloadToFile(segURL, segFilePath).then(() => { + const arrayBuffer = fs.readFileSync(segFilePath) + .buffer; + const dicomDict = DicomMessage.readFile( + arrayBuffer + ); + const dataset = DicomMetaDictionary.naturalizeDataset( + dicomDict.dict + ); + const multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset( + [dataset] + ); + expect(dataset.NumberOfFrames).to.equal(1); + expect(multiframe.NumberOfFrames).to.equal(1); + console.log("Finished test_oneslice_seg"); + }); + }); + }) + ); + }); }, test_multiframe_us: () => { - const file = fs.readFileSync(path.join(__dirname, 'cine-test.dcm')); + const file = fs.readFileSync(path.join(__dirname, "cine-test.dcm")); const dicomData = dcmjs.data.DicomMessage.readFile(file.buffer, { // ignoreErrors: true, }); - const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict); + const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( + dicomData.dict + ); // eslint-disable-next-line no-underscore-dangle - dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta); + dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset( + dicomData.meta + ); expect(dataset.NumberOfFrames).to.equal(8); - console.log("Finished test_multiframe_us") + console.log("Finished test_multiframe_us"); }, test_null_number_vrs: () => { - const dicomDict = new DicomDict({ TransferSynxtaxUID: "1.2.840.10008.1.2.1" }); - dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(datasetWithNullNumberVRs); + 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); + const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( + dicomData.dict + ); expect(dataset.ImageAndFluoroscopyAreaDoseProduct).to.equal(0); expect(dataset.InstanceNumber).to.equal(0); @@ -252,11 +289,11 @@ const tests = { }, test_output_equality: () => { - const file = fs.readFileSync(path.join(__dirname, 'cine-test.dcm')); + const file = fs.readFileSync(path.join(__dirname, "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, @@ -265,20 +302,20 @@ const tests = { check_equality(dicomData1.meta, dicomData2.meta); check_equality(dicomData1.dict, dicomData2.dict); - console.log("Finished test_output_equality") + console.log("Finished test_output_equality"); function check_equality(dict1, dict2) { Object.keys(dict1).forEach(key => { const elem1 = dict1[key]; - const elem2 = dict2[key] + const elem2 = dict2[key]; expect(JSON.stringify(elem1)).to.equal(JSON.stringify(elem2)); - }) + }); } }, test_performance: async () => { - const file = fs.readFileSync(path.join(__dirname, 'cine-test.dcm')); + const file = fs.readFileSync(path.join(__dirname, "cine-test.dcm")); let buffer = file.buffer; let json; const start = Date.now(); @@ -297,25 +334,46 @@ const tests = { function check_equality(dict1, dict2) { Object.keys(dict1).forEach(key => { const elem1 = dict1[key]; - const elem2 = dict2[key] + const elem2 = dict2[key]; expect(JSON.stringify(elem1)).to.equal(JSON.stringify(elem2)); - }) + }); } - console.log(`Finished. Total Time elapsed: ${Date.now() - start} ms`) + console.log(`Finished. Total Time elapsed: ${Date.now() - start} ms`); + }, + + test_invalid_vr_length: () => { + const file = fs.readFileSync( + path.join(__dirname, "invalid-vr-length-test.dcm") + ); + const dicomDict = dcmjs.data.DicomMessage.readFile(file.buffer); + + expect(() => + writeToBuffer(dicomDict, { allowInvalidVRLength: false }) + ).to.throw(); + expect(() => + writeToBuffer(dicomDict, { allowInvalidVRLength: true }) + ).not.to.throw(); + + function writeToBuffer(dicomDict, options) { + return dicomDict.write(options); + } } -} +}; -exports.test = async (testToRun) => { +exports.test = async testToRun => { Object.keys(tests).forEach(testName => { - if (testToRun && !testName.toLowerCase().includes(testToRun.toLowerCase())) { + if ( + testToRun && + !testName.toLowerCase().includes(testToRun.toLowerCase()) + ) { console.log("-- Skipping " + testName); return false; } console.log("-- Starting " + testName); tests[testName](); }); -} +}; exports.tests = tests;