diff --git a/.jshintrc b/.jshintrc index 2b6f469f..711f4c4f 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,3 +1,3 @@ { - "esversion": 6 + "esversion": 11 } diff --git a/docs/BreakingChanges.md b/docs/BreakingChanges.md new file mode 100644 index 00000000..f9ebb8d6 --- /dev/null +++ b/docs/BreakingChanges.md @@ -0,0 +1,55 @@ +# Migration Guide for Breaking Changes + +## v 0.50.1 to higher + +After version 0.50.1, we introduced a breaking change to `WriteBufferStream` and `ReadBufferStream`. + +### What Changed + +- `ReadBufferStream`: Constructor signature changed from +```js +constructor( + buffer, + littleEndian = false, + options = { + start: null, + stop: null, + noCopy: false + }, + encoding = defaultDICOMEncoding + ) +``` +to +```js +constructor( + buffer, + options = { + start: null, + stop: null, + encoding: defaultDICOMEncoding, + noCopy: false, + littleEndian: true + } +) +``` + +- `WriteBufferStream`: Constructor signature changed from +```js +constructor(defaultSize, options = null) +``` +to +```js +constructor(options = null) +``` + +### Notes + +Essentially, the options that used to be separate parameters were moved into the flexible `options` object. + +For reading situations, the `littleEndian` and `encoding` options are now in that options object. Moving forward, any +new parameters will be introduced as options here. + +For writing situations, the `defaultSize` parameter was moved into the options object. + +In all situations, the legacy usage of the constructor will fire a warning reminding you of the change to prompt you to +update your usage of the library. diff --git a/src/AsyncDicomReader.js b/src/AsyncDicomReader.js index d3dda2f1..aa4aa32a 100644 --- a/src/AsyncDicomReader.js +++ b/src/AsyncDicomReader.js @@ -7,16 +7,17 @@ import { VM_DELIMITER, UNDEFINED_LENGTH, TagHex, - encodingMapping, UNDEFINED_LENGTH_FIX, VALID_VRS, + singleVRs, isVideoTransferSyntax } from "./constants/dicom"; +import { encodingMapping } from "./constants/encodings"; import { Tag } from "./Tag"; -import { DicomMessage, singleVRs } from "./DicomMessage"; +import { DicomMessage } from "./DicomMessage"; import { DicomMetaDictionary } from "./DicomMetaDictionary"; import { DicomMetadataListener } from "./utilities/DicomMetadataListener.js"; -import { log } from "./log.js"; +import { log } from "./utilities/log.js"; const readLog = log.getLogger("AsyncDicomReader"); @@ -35,7 +36,8 @@ export class AsyncDicomReader { this.isLittleEndian = options?.isLittleEndian; // Default maxFragmentSize is 128 MB (128 * 1024 * 1024 bytes) this.maxFragmentSize = options?.maxFragmentSize ?? 128 * 1024 * 1024; - this.stream = new ReadBufferStream(null, this.isLittleEndian, { + this.stream = new ReadBufferStream(null, { + littleEncoding: this.isLittleEndian, clearBuffers: true, ...options }); @@ -720,13 +722,18 @@ export class AsyncDicomReader { } } else { const value = vr.read(stream, length, syntax)?.value; - if (!vr.isBinary() && singleVRs.indexOf(vr.type) == -1) { + if (!vr.isBinary() && !singleVRs.has(vr.type)) { values = value; if (typeof value === "string") { const delimiterChar = String.fromCharCode(VM_DELIMITER); values = vr.dropPadByte(value.split(delimiterChar)); } - } else if (vr.type == "OW" || vr.type == "OB") { + } else if ( + vr.type === "OW" || + vr.type === "OB" || + vr.type === "OD" || + vr.type === "OF" + ) { values = value; } else { Array.isArray(value) ? (values = value) : values.push(value); @@ -737,9 +744,8 @@ export class AsyncDicomReader { if (values.length > 0) { let [coding] = values; coding = coding.replace(/[_ ]/g, "-").toLowerCase(); - if (coding in encodingMapping) { - coding = encodingMapping[coding]; - this.stream.setDecoder(new TextDecoder(coding)); + if (encodingMapping.has(coding)) { + this.stream.setEncoding(coding); } else if (options?.ignoreErrors) { log.warn( `Unsupported character set: ${coding}, using default character set` diff --git a/src/BufferStream.js b/src/BufferStream.js index e9acf7ec..12c76645 100644 --- a/src/BufferStream.js +++ b/src/BufferStream.js @@ -2,11 +2,14 @@ import pako from "pako"; import SplitDataView from "./SplitDataView"; import { toFloat } from "./utilities/toFloat"; import { toInt } from "./utilities/toInt"; +import { DicomTextTranscode } from "./DicomTextTranscode"; +import { defaultDICOMEncoding } from "./constants/encodings"; +import { log } from "./utilities/log"; export class BufferStream { offset = 0; startOffset = 0; - isLittleEndian = false; + isLittleEndian = true; size = 0; view = new SplitDataView(); /** The available listeners are those waiting for a query response */ @@ -18,12 +21,14 @@ export class BufferStream { /** A flag to set to indicate to clear buffers as they get consumed */ clearBuffers = false; - encoder = new TextEncoder("utf-8"); + textTranscoder = new DicomTextTranscode(); constructor(options = null) { - this.isLittleEndian = options?.littleEndian || this.isLittleEndian; + this.isLittleEndian = options?.littleEndian ?? this.isLittleEndian; this.view.defaultSize = options?.defaultSize ?? this.view.defaultSize; - this.clearBuffers = options.clearBuffers || false; + this.clearBuffers = options?.clearBuffers || false; + + this.setEncoding(options?.encoding || defaultDICOMEncoding); } /** @@ -68,8 +73,20 @@ export class BufferStream { return true; } - setEndian(isLittle) { - this.isLittleEndian = isLittle; + setEncoding(dicomEncoding, ignoreErrors) { + this.textTranscoder.setEncoding(dicomEncoding, ignoreErrors); + } + + setEndian(isLittleEndian = true) { + this.isLittleEndian = isLittleEndian; + } + + setLittleEndian() { + this.isLittleEndian = true; + } + + setBigEndian() { + this.isLittleEndian = false; } slice(start = this.startOffset, end = this.endOffset) { @@ -175,7 +192,7 @@ export class BufferStream { } writeUTF8String(value) { - const encodedString = this.encoder.encode(value); + const encodedString = this.textTranscoder.encode(value); this.checkSize(encodedString.byteLength); this.view.writeBuffer(encodedString, this.offset); return this.increment(encodedString.byteLength); @@ -312,7 +329,7 @@ export class BufferStream { const view = new DataView( this.slice(this.offset, this.offset + length) ); - const result = this.decoder.decode(view); + const result = this.textTranscoder.decode(view); this.increment(length); return result; } @@ -465,35 +482,49 @@ export class BufferStream { export class ReadBufferStream extends BufferStream { constructor( buffer, - littleEndian, options = { start: null, stop: null, - noCopy: false + encoding: defaultDICOMEncoding, + noCopy: false, + littleEndian: true } ) { - super({ ...options, littleEndian }); - this.noCopy = options.noCopy; - this.decoder = new TextDecoder("latin1"); + options instanceof Object + ? {} + : log.warn( + "The constructor API for ReadBufferStream has changed to include the" + + " littleEndian option as part of the options object. Please, update your usage of the class. We are " + + "using defaults now. See docs/BreakingChanges.md for more details." + ); + const optionsOptions = + options instanceof Object + ? options + : { + start: null, + stop: null, + encoding: defaultDICOMEncoding, + noCopy: false, + littleEndian: options + }; + super(optionsOptions); + this.noCopy = optionsOptions.noCopy; if (buffer instanceof BufferStream) { - this.view.from(buffer.view, options); + this.view.from(buffer.view, optionsOptions); this.isComplete = true; } else if (buffer) { this.view.addBuffer(buffer); this.isComplete = true; } - this.offset = options.start ?? buffer?.offset ?? 0; - this.size = options.stop || buffer?.size || buffer?.byteLength || 0; + this.offset = optionsOptions.start ?? buffer?.offset ?? 0; + this.size = + optionsOptions.stop || buffer?.size || buffer?.byteLength || 0; this.startOffset = this.offset; this.endOffset = this.size; } - setDecoder(decoder) { - this.decoder = decoder; - } - reset() { this.offset = this.startOffset; return this; @@ -571,13 +602,30 @@ export class DeflatedReadBufferStream extends ReadBufferStream { const inflatedBuffer = pako.inflateRaw( stream.getBuffer(stream.offset, stream.size) ); - super(inflatedBuffer.buffer, stream.littleEndian, options); + super(inflatedBuffer.buffer, { + littleEndian: stream.littleEndian, + ...options + }); } } export class WriteBufferStream extends BufferStream { - constructor(defaultSize, littleEndian) { - super({ defaultSize, littleEndian }); + constructor(options = null) { + options instanceof Object + ? {} + : log.warn( + "The constructor API for WriteBufferStream has changed to include the" + + " littleEndian option as part of the options object. Please, update your usage of the class. We are " + + "using defaults now. See docs/BreakingChanges.md for more details." + ); + const optionsOptions = + options instanceof Object + ? options + : { + encoding: defaultDICOMEncoding, + littleEndian: options + }; + super(optionsOptions); this.size = 0; } } diff --git a/src/DicomDict.js b/src/DicomDict.js index 2ca8c8fb..29c92dd4 100644 --- a/src/DicomDict.js +++ b/src/DicomDict.js @@ -22,13 +22,24 @@ class DicomDict { } } - write(writeOptions = { allowInvalidVRLength: false }) { - var metaSyntax = EXPLICIT_LITTLE_ENDIAN; - var fileStream = new WriteBufferStream(4096, true); + write( + writeOptions = { + allowInvalidVRLength: false + } + ) { + const metaSyntax = EXPLICIT_LITTLE_ENDIAN; + const fileStream = new WriteBufferStream({ + defaultSize: 4096, + littleEndian: writeOptions.littleEndian + }); + fileStream.writeUint8Repeat(0, 128); fileStream.writeAsciiString("DICM"); - var metaStream = new WriteBufferStream(1024); + const metaStream = new WriteBufferStream({ + defaultSize: 1024, + littleEndian: writeOptions.littleEndian + }); if (!this.meta[TagHex.TransferSyntaxUID]) { this.meta[TagHex.TransferSyntaxUID] = { vr: "UI", @@ -46,7 +57,7 @@ class DicomDict { ); fileStream.concat(metaStream); - var useSyntax = this.meta[TagHex.TransferSyntaxUID].Value[0]; + const useSyntax = this.meta[TagHex.TransferSyntaxUID].Value[0]; DicomMessage.write(this.dict, fileStream, useSyntax, writeOptions); return fileStream.getBuffer(); } diff --git a/src/DicomMessage.js b/src/DicomMessage.js index 95318cc1..b9d8163a 100644 --- a/src/DicomMessage.js +++ b/src/DicomMessage.js @@ -6,18 +6,18 @@ import { IMPLICIT_LITTLE_ENDIAN, VM_DELIMITER, TagHex, - encodingMapping, + singleVRs, unencapsulatedTransferSyntaxes, UNDEFINED_LENGTH } from "./constants/dicom.js"; import { DicomDict } from "./DicomDict.js"; import { DicomMetaDictionary } from "./DicomMetaDictionary.js"; import { Tag } from "./Tag.js"; -import { log } from "./log.js"; +import { log } from "./utilities/log.js"; import { deepEqual } from "./utilities/deepEqual"; import { ValueRepresentation } from "./ValueRepresentation.js"; - -export const singleVRs = ["SQ", "OF", "OW", "OB", "UN", "LT"]; +import { defaultDICOMEncoding } from "./constants/encodings"; +import { selectDICOMEncoding } from "./utilities/selectEncoding"; export class DicomMessage { static read( @@ -59,12 +59,12 @@ export class DicomMessage { } ) { const { ignoreErrors, untilTag, stopOnGreaterTag } = options; - var dict = {}; + let dict = {}; try { let previousTagOffset; while (!bufferStream.end()) { previousTagOffset = bufferStream.offset; - const readInfo = DicomMessage._readTag( + let readInfo = DicomMessage._readTag( bufferStream, syntax, options @@ -75,33 +75,19 @@ export class DicomMessage { break; } if (cleanTagString === TagHex.SpecificCharacterSet) { - if (readInfo.values.length > 0) { - let coding = readInfo.values[0]; - coding = coding.replace(/[_ ]/g, "-").toLowerCase(); - if (coding in encodingMapping) { - coding = encodingMapping[coding]; - bufferStream.setDecoder(new TextDecoder(coding)); - } else if (ignoreErrors) { - log.warn( - `Unsupported character set: ${coding}, using default character set` - ); - } else { - throw Error(`Unsupported character set: ${coding}`); - } - } - if (readInfo.values.length > 1) { - if (ignoreErrors) { - log.warn( - "Using multiple character sets is not supported, proceeding with just the first character set", - readInfo.values - ); - } else { - throw Error( - `Using multiple character sets is not supported: ${readInfo.values}` - ); - } - } - readInfo.values = ["ISO_IR 192"]; // change SpecificCharacterSet to UTF-8 + // Note, I have seen a switch of encoding when entering SR nodes. + // Watch out for potential corruption when reading. Since this library does not currently have a + // way to detect when returning from a node, I cannot currently restore the previous context encoding. + // TODO: Add a check for end of SR node and presence of encoding tag => create a global stack and pop last encoding. + const encoding = selectDICOMEncoding( + readInfo.values, + ignoreErrors + ); + bufferStream.setEncoding(encoding, ignoreErrors); + + // Are we resetting the encoding here because the stream will decode the input buffer from source + // encoding to UTF-8? + readInfo.values = defaultDICOMEncoding; // change SpecificCharacterSet to UTF-8 } dict[cleanTagString] = ValueRepresentation.addTagAccessors({ @@ -126,9 +112,9 @@ export class DicomMessage { static _normalizeSyntax(syntax) { if ( - syntax == IMPLICIT_LITTLE_ENDIAN || - syntax == EXPLICIT_LITTLE_ENDIAN || - syntax == EXPLICIT_BIG_ENDIAN + syntax === IMPLICIT_LITTLE_ENDIAN || + syntax === EXPLICIT_LITTLE_ENDIAN || + syntax === EXPLICIT_BIG_ENDIAN ) { return syntax; } else { @@ -150,10 +136,11 @@ export class DicomMessage { forceStoreRaw: false } ) { - var stream = new ReadBufferStream(buffer, null, { - noCopy: options.noCopy - }), - useSyntax = EXPLICIT_LITTLE_ENDIAN; + let stream = new ReadBufferStream(buffer, { + noCopy: options.noCopy + }); + const useSyntax = EXPLICIT_LITTLE_ENDIAN; + stream.reset(); stream.increment(128); if (stream.readAsciiString(4) !== "DICM") { @@ -161,12 +148,12 @@ export class DicomMessage { } // save position before reading first tag - var metaStartPos = stream.offset; + const metaStartPos = stream.offset; // read the first tag to check if it's the meta length tag - var el = DicomMessage._readTag(stream, useSyntax); + const el = DicomMessage._readTag(stream, useSyntax); - var metaHeader = {}; + let metaHeader = {}; if (el.tag.cleanString !== TagHex.FileMetaInformationGroupLength) { // meta length tag is missing if (!options.ignoreErrors) { @@ -186,15 +173,15 @@ export class DicomMessage { }); } else { // meta length tag is present - var metaLength = el.values[0]; + const metaLength = el.values[0]; // read header buffer using the specified meta length - var metaStream = stream.more(metaLength); + const metaStream = stream.more(metaLength); metaHeader = DicomMessage._read(metaStream, useSyntax, options); } //get the syntax - var mainSyntax = metaHeader[TagHex.TransferSyntaxUID].Value[0]; + let mainSyntax = metaHeader[TagHex.TransferSyntaxUID].Value[0]; //in case of deflated dataset, decompress and continue if (mainSyntax === DEFLATED_EXPLICIT_LITTLE_ENDIAN) { @@ -204,30 +191,30 @@ export class DicomMessage { } mainSyntax = DicomMessage._normalizeSyntax(mainSyntax); - var objects = DicomMessage._read(stream, mainSyntax, options); + const objects = DicomMessage._read(stream, mainSyntax, options); - var dicomDict = new DicomDict(metaHeader); + const dicomDict = new DicomDict(metaHeader); dicomDict.dict = objects; return dicomDict; } static writeTagObject(stream, tagString, vr, values, syntax, writeOptions) { - var tag = Tag.fromString(tagString); + const tag = Tag.fromString(tagString); tag.write(stream, vr, values, syntax, writeOptions); } static write(jsonObjects, useStream, syntax, writeOptions) { - var written = 0; + let written = 0; - var sortedTags = Object.keys(jsonObjects).sort(); + const sortedTags = Object.keys(jsonObjects).sort(); sortedTags.forEach(function (tagString) { - var tag = Tag.fromString(tagString), + const tag = Tag.fromString(tagString), tagObject = jsonObjects[tagString], vrType = tagObject.vr; - var values = DicomMessage._getTagWriteValues(vrType, tagObject); + const values = DicomMessage._getTagWriteValues(vrType, tagObject); written += tag.write( useStream, @@ -275,16 +262,14 @@ export class DicomMessage { } ) { const { untilTag, includeUntilTagValue } = options; - var implicit = syntax == IMPLICIT_LITTLE_ENDIAN ? true : false, + const implicit = syntax === IMPLICIT_LITTLE_ENDIAN, isLittleEndian = - syntax == IMPLICIT_LITTLE_ENDIAN || - syntax == EXPLICIT_LITTLE_ENDIAN - ? true - : false; + syntax === IMPLICIT_LITTLE_ENDIAN || + syntax === EXPLICIT_LITTLE_ENDIAN; - var oldEndian = stream.isLittleEndian; + const oldEndian = stream.isLittleEndian; stream.setEndian(isLittleEndian); - var tag = Tag.readTag(stream); + const tag = Tag.readTag(stream); if (untilTag === tag.toCleanString() && untilTag !== null) { if (!includeUntilTagValue) { @@ -292,22 +277,22 @@ export class DicomMessage { } } - var length = null, + let length = null, vr = null, vrType; if (implicit) { length = stream.readUint32(); - var elementData = DicomMessage.lookupTag(tag); + const elementData = DicomMessage.lookupTag(tag); if (elementData) { vrType = elementData.vr; } else { //unknown tag - if (length == UNDEFINED_LENGTH) { + if (length === UNDEFINED_LENGTH) { vrType = "SQ"; } else if (tag.isPixelDataTag()) { vrType = "OW"; - } else if (vrType == "xs") { + } else if (vrType === "xs") { vrType = "US"; } else if (tag.isPrivateCreator()) { vrType = "LO"; @@ -339,11 +324,11 @@ export class DicomMessage { } } - var values = []; - var rawValues = []; + let values = []; + let rawValues = []; if (vr.isBinary() && length > vr.maxLength && !vr.noMultiple) { - var times = length / vr.maxLength, - i = 0; + const times = length / vr.maxLength; + let i = 0; while (i++ < times) { const { rawValue, value } = vr.read( stream, @@ -357,7 +342,7 @@ export class DicomMessage { } else { const { rawValue, value } = vr.read(stream, length, syntax, options) || {}; - if (!vr.isBinary() && singleVRs.indexOf(vr.type) == -1) { + if (!vr.isBinary() && !singleVRs.has(vr.type)) { rawValues = rawValue; values = value; if (typeof value === "string") { @@ -365,10 +350,10 @@ export class DicomMessage { rawValues = vr.dropPadByte(rawValue.split(delimiterChar)); values = vr.dropPadByte(value.split(delimiterChar)); } - } else if (vr.type == "SQ") { + } else if (vr.type === "SQ") { rawValues = rawValue; values = value; - } else if (vr.type == "OW" || vr.type == "OB") { + } else if (vr.type === "OW" || vr.type === "OB") { rawValues = rawValue; values = value; } else { diff --git a/src/DicomMetaDictionary.js b/src/DicomMetaDictionary.js index 6a8132ba..1296582c 100644 --- a/src/DicomMetaDictionary.js +++ b/src/DicomMetaDictionary.js @@ -1,8 +1,14 @@ import { dictionary } from "./dictionary.fast.js"; import { getAllStandardTagEntries } from "./dicom.lookup.js"; -import log from "./log.js"; +import log from "./utilities/log.js"; import addAccessors from "./utilities/addAccessors"; import { ValueRepresentation } from "./ValueRepresentation"; +import { encapsulatedSyntaxes } from "./constants/syntaxes"; +import { encodingMapping } from "./constants/encodings"; +import { + sopClassNamesByUID, + sopClassUIDsByName +} from "./constants/sopClassUIDs"; export class DicomMetaDictionary { // intakes a custom dictionary that will be used to parse/denaturalize the dataset @@ -17,7 +23,7 @@ export class DicomMetaDictionary { return rawTag; } if (rawTag.length === 8 && rawTag === rawTag.match(/[0-9a-fA-F]*/)[0]) { - var tag = rawTag.toUpperCase(); + const tag = rawTag.toUpperCase(); return "(" + tag.substring(0, 4) + "," + tag.substring(4, 8) + ")"; } } @@ -38,7 +44,7 @@ export class DicomMetaDictionary { static tagAsIntegerFromName(name) { const item = DicomMetaDictionary.nameMap[name]; - if (item != undefined) { + if (item !== undefined) { return this.parseIntFromTag(item.tag); } else { return undefined; @@ -52,7 +58,7 @@ export class DicomMetaDictionary { const cleanedDataset = {}; Object.keys(dataset).forEach(tag => { const data = Object.assign({}, dataset[tag]); - if (data.vr == "SQ") { + if (data.vr === "SQ") { const cleanedValues = []; Object.keys(data.Value).forEach(index => { cleanedValues.push( @@ -64,7 +70,7 @@ export class DicomMetaDictionary { // remove null characters from strings data.Value = Object.keys(data.Value).map(index => { const item = data.Value[index]; - if (item.constructor.name == "String") { + if (item.constructor.name === "String") { return item.replace(/\0/, ""); } return item; @@ -82,7 +88,7 @@ export class DicomMetaDictionary { var namedDataset = {}; Object.keys(dataset).forEach(tag => { const data = Object.assign({}, dataset[tag]); - if (data.vr == "SQ") { + if (data.vr === "SQ") { var namedValues = []; Object.keys(data.Value).forEach(index => { namedValues.push( @@ -124,6 +130,7 @@ export class DicomMetaDictionary { if (entry) { naturalName = entry.name; + // Keep as == don't check for strict equality... if (entry.vr == "ox") { // when the vr is data-dependent, keep track of the original type naturalDataset._vrMap[naturalName] = data.vr; @@ -202,7 +209,7 @@ export class DicomMetaDictionary { } value = value.map(entry => - entry.constructor.name == "Number" ? String(entry) : entry + entry.constructor.name === "Number" ? String(entry) : entry ); return value; @@ -210,11 +217,11 @@ export class DicomMetaDictionary { // keep the static function to support previous calls to the class static denaturalizeDataset(dataset, nameMap = DicomMetaDictionary.nameMap) { - var unnaturalDataset = {}; + let unnaturalDataset = {}; Object.keys(dataset).forEach(naturalName => { // check if it's a sequence - var name = naturalName; - var entry = nameMap[name]; + const name = naturalName; + const entry = nameMap[name]; if (entry) { let dataValue = dataset[naturalName]; @@ -228,12 +235,12 @@ export class DicomMetaDictionary { ? dataset._vrMap[naturalName] : entry.vr; - var dataItem = ValueRepresentation.addTagAccessors({ vr }); + const dataItem = ValueRepresentation.addTagAccessors({ vr }); dataItem.Value = dataset[naturalName]; if (dataValue !== null) { - if (entry.vr == "ox") { + if (entry.vr === "ox") { if (dataset._vrMap && dataset._vrMap[naturalName]) { dataItem.vr = dataset._vrMap[naturalName]; } else { @@ -252,8 +259,8 @@ export class DicomMetaDictionary { dataItem.Value ); - if (entry.vr == "SQ") { - var unnaturalValues = []; + if (entry.vr === "SQ") { + let unnaturalValues = []; for ( let datasetIndex = 0; datasetIndex < dataItem.Value.length; @@ -289,11 +296,11 @@ export class DicomMetaDictionary { } } - var tag = DicomMetaDictionary.unpunctuateTag(entry.tag); + const tag = DicomMetaDictionary.unpunctuateTag(entry.tag); unnaturalDataset[tag] = dataItem; } else { const validMetaNames = ["_vrMap", "_meta"]; - if (validMetaNames.indexOf(name) == -1) { + if (validMetaNames.indexOf(name) === -1) { log.warn( "Unknown name in dataset", name, @@ -376,22 +383,14 @@ export class DicomMetaDictionary { return nameMap; } Object.keys(dictionary).forEach(tag => { - var dict = dictionary[tag]; - if (dict && dict.version != "PrivateTag") { + const dict = dictionary[tag]; + if (dict && dict.version !== "PrivateTag") { nameMap[dict.name] = dict; } }); return nameMap; } - static _generateUIDMap() { - DicomMetaDictionary.sopClassUIDsByName = {}; - Object.keys(DicomMetaDictionary.sopClassNamesByUID).forEach(uid => { - var name = DicomMetaDictionary.sopClassNamesByUID[uid]; - DicomMetaDictionary.sopClassUIDsByName[name] = uid; - }); - } - // denaturalizes dataset using custom dictionary and nameMap denaturalizeDataset(dataset) { return DicomMetaDictionary.denaturalizeDataset( @@ -401,40 +400,21 @@ export class DicomMetaDictionary { } } -// Subset of those listed at: -// http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_B.5 -DicomMetaDictionary.sopClassNamesByUID = { - "1.2.840.10008.5.1.4.1.1.20": "NMImage", - "1.2.840.10008.5.1.4.1.1.2": "CTImage", - "1.2.840.10008.5.1.4.1.1.2.1": "EnhancedCTImage", - "1.2.840.10008.5.1.4.1.1.2.2": "LegacyConvertedEnhancedCTImage", - "1.2.840.10008.5.1.4.1.1.3.1": "USMultiframeImage", - "1.2.840.10008.5.1.4.1.1.4": "MRImage", - "1.2.840.10008.5.1.4.1.1.4.1": "EnhancedMRImage", - "1.2.840.10008.5.1.4.1.1.4.2": "MRSpectroscopy", - "1.2.840.10008.5.1.4.1.1.4.3": "EnhancedMRColorImage", - "1.2.840.10008.5.1.4.1.1.4.4": "LegacyConvertedEnhancedMRImage", - "1.2.840.10008.5.1.4.1.1.6.1": "USImage", - "1.2.840.10008.5.1.4.1.1.6.2": "EnhancedUSVolume", - "1.2.840.10008.5.1.4.1.1.7": "SecondaryCaptureImage", - "1.2.840.10008.5.1.4.1.1.30": "ParametricMapStorage", - "1.2.840.10008.5.1.4.1.1.66": "RawData", - "1.2.840.10008.5.1.4.1.1.66.1": "SpatialRegistration", - "1.2.840.10008.5.1.4.1.1.66.2": "SpatialFiducials", - "1.2.840.10008.5.1.4.1.1.66.3": "DeformableSpatialRegistration", - "1.2.840.10008.5.1.4.1.1.66.4": "Segmentation", - "1.2.840.10008.5.1.4.1.1.66.7": "LabelmapSegmentation", // Labelmap Segmentation SOP Class UID - "1.2.840.10008.5.1.4.1.1.67": "RealWorldValueMapping", - "1.2.840.10008.5.1.4.1.1.88.11": "BasicTextSR", - "1.2.840.10008.5.1.4.1.1.88.22": "EnhancedSR", - "1.2.840.10008.5.1.4.1.1.88.33": "ComprehensiveSR", - "1.2.840.10008.5.1.4.1.1.88.34": "Comprehensive3DSR", - "1.2.840.10008.5.1.4.1.1.128": "PETImage", - "1.2.840.10008.5.1.4.1.1.130": "EnhancedPETImage", - "1.2.840.10008.5.1.4.1.1.128.1": "LegacyConvertedEnhancedPETImage", - "1.2.840.10008.5.1.4.1.1.77.1.5.1": "OphthalmicPhotography8BitImage", - "1.2.840.10008.5.1.4.1.1.77.1.5.4": "OphthalmicTomographyImage" -}; +/** + * Add a dicom encoding to main encoding map => `encodingMapping` from `utils/constants/encodings`. + * + * @param dicomEncoding + * @param webEncoding + * @returns {Map} + */ +DicomMetaDictionary.addEncoding = (dicomEncoding, webEncoding) => + encodingMapping.set(dicomEncoding, webEncoding); + +// TODO: Is this assignment necessary? +DicomMetaDictionary.sopClassUIDsByName = sopClassUIDsByName; +DicomMetaDictionary.sopClassNamesByUID = sopClassNamesByUID; +DicomMetaDictionary.encapsulatedSyntaxes = encapsulatedSyntaxes; +DicomMetaDictionary.encodingMapping = encodingMapping; // Avoid loops in imports ValueRepresentation.setDicomMetaDictionary(DicomMetaDictionary); @@ -442,4 +422,3 @@ ValueRepresentation.setDicomMetaDictionary(DicomMetaDictionary); DicomMetaDictionary.dictionary = dictionary; DicomMetaDictionary._generateNameMap(); -DicomMetaDictionary._generateUIDMap(); diff --git a/src/DicomTextTranscode.js b/src/DicomTextTranscode.js new file mode 100644 index 00000000..a2729229 --- /dev/null +++ b/src/DicomTextTranscode.js @@ -0,0 +1,74 @@ +import { defaultEncoding } from "./constants/encodings"; +import { selectNativeEncoding } from "./utilities/selectEncoding"; + +/** + * Facilitates the conversion of binary buffers from a DICOM encoding scheme to + * a web supported string encoding scheme and vice versa. + */ +export class DicomTextTranscode { + encoder = new TextEncoder(defaultEncoding); + decoder = new TextDecoder(defaultEncoding); + + /** + * Use this method if you want to change the decoder directly and do not + * need to have the encoding scheme name translated to one of the encoding + * schemes supported by web browsers. + * + * For example, instead of passing ISO 2022 IR 100, you have to pass latin1. + * Passing an incorrect encoding scheme name will result in an exception. + * + * @param {string} webEncoding + */ + setNativeDecoder(webEncoding) { + this.decoder = new TextDecoder(webEncoding); + } + + /** + * Unused since we typically default to utf-8. This method is provided for + * convenience in case someone needs to encode a buffer in something else + * before storing in a DICOM header. + * + * @param {string} webEncoding + */ + setNativeEncoder(webEncoding) { + this.decoder = new TextEncoder(webEncoding); + } + + /** + * Main method for changing encoder. + * + * Given a DICOM encoding scheme like ISO 2022 IR 100, generate the correct + * string to store to disk. + * + * Optionally, include whether to ignore or throw an exception if dicom to + * web encoding is not found in our mapping + * + * @param {string} dicomEncoding + * @param {boolean} ignoreErrors + */ + setEncoding(dicomEncoding, ignoreErrors = false) { + let coding = selectNativeEncoding(dicomEncoding, ignoreErrors); + this.setNativeEncoder(coding); + this.setNativeDecoder(coding); + } + + /** + * Convenience method as would be found in TextEncoder and TextDecoder APIs. + * + * @param data + * @returns {string} + */ + decode(data) { + return this.decoder.decode(data); + } + + /** + * Convenience method as would be found in TextEncoder and TextDecoder APIs. + * + * @param {string} data + * @returns {Uint8Array} + */ + encode(data) { + return this.encoder.encode(data); + } +} diff --git a/src/Tag.js b/src/Tag.js index 2d755b8a..11457036 100644 --- a/src/Tag.js +++ b/src/Tag.js @@ -86,13 +86,13 @@ class Tag { } static fromString(str) { - var group = parseInt(str.substring(0, 4), 16), + const group = parseInt(str.substring(0, 4), 16), element = parseInt(str.substring(4), 16); return Tag.fromNumbers(group, element); } static fromPString(str) { - var group = parseInt(str.substring(1, 5), 16), + const group = parseInt(str.substring(1, 5), 16), element = parseInt(str.substring(6, 10), 16); return Tag.fromNumbers(group, element); } @@ -102,7 +102,7 @@ class Tag { } static readTag(stream) { - var group = stream.readUint16(), + const group = stream.readUint16(), element = stream.readUint16(); return Tag.fromNumbers(group, element); } @@ -149,11 +149,14 @@ class Tag { stream.writeUint16(this.group()); stream.writeUint16(this.element()); - var tagStream = new WriteBufferStream(256), - valueLength; - tagStream.setEndian(isLittleEndian); + const tagStream = new WriteBufferStream({ + defaultSize: 256, + littleEndian: isLittleEndian, + encoding: writeOptions?.encoding + }); + let valueLength; - if (vrType == "OW" || vrType == "OB" || vrType == "UN") { + if (vrType === "OW" || vrType === "OB" || vrType === "UN") { valueLength = vr.writeBytes( tagStream, values, @@ -161,7 +164,7 @@ class Tag { isEncapsulated, writeOptions ); - } else if (vrType == "SQ") { + } else if (vrType === "SQ") { valueLength = vr.writeBytes( tagStream, values, @@ -172,10 +175,10 @@ class Tag { valueLength = vr.writeBytes(tagStream, values, writeOptions); } - if (vrType == "SQ") { + if (vrType === "SQ") { valueLength = UNDEFINED_LENGTH; } - var written = tagStream.size + 4; + let written = tagStream.size + 4; if (implicit) { stream.writeUint32(valueLength); diff --git a/src/ValueRepresentation.js b/src/ValueRepresentation.js index 3be9a189..efdd551d 100644 --- a/src/ValueRepresentation.js +++ b/src/ValueRepresentation.js @@ -5,8 +5,9 @@ import { PN_COMPONENT_DELIMITER, VM_DELIMITER } from "./constants/dicom.js"; -import { log, validationLog } from "./log.js"; +import { log, validationLog } from "./utilities/log.js"; import dicomJson from "./utilities/dicomJson.js"; +import { binaryVRs, singleVRs, length32VRs } from "./constants/dicom.js"; // We replace the tag with a Proxy which intercepts assignments to obj[valueProp] // and adds additional overrides/accessors to the value if need be. If valueProp @@ -25,7 +26,7 @@ import dicomJson from "./utilities/dicomJson.js"; // TODO: refactor with addAccessors.js in mind const tagProxyHandler = { set(target, prop, value) { - var vrType; + let vrType; if ( ["values", "Value"].includes(prop) && target.vr && @@ -65,18 +66,13 @@ function toWindows(inputArray, size) { let DicomMessage, Tag, DicomMetaDictionary; -var binaryVRs = ["FL", "FD", "SL", "SS", "UL", "US", "AT", "UV"], - length32VRs = ["OB", "OW", "OF", "SQ", "UC", "UR", "UT", "UN", "OD", "UV"], - singleVRs = ["SQ", "OF", "OW", "OB", "UN"]; - class ValueRepresentation { constructor(type) { this.type = type; this.multi = false; - this._isBinary = binaryVRs.indexOf(this.type) != -1; - this._allowMultiple = - !this._isBinary && singleVRs.indexOf(this.type) == -1; - this._isLength32 = length32VRs.indexOf(this.type) != -1; + this._isBinary = binaryVRs.has(this.type); + this._allowMultiple = !this._isBinary && !singleVRs.has(this.type); + this._isLength32 = length32VRs.has(this.type); this._storeRaw = true; } @@ -230,7 +226,7 @@ class ValueRepresentation { if (stream.peekUint8(length - 1) !== this.padByte) { return stream.readAsciiString(length); } else { - var val = stream.readAsciiString(length - 1); + const val = stream.readAsciiString(length - 1); stream.increment(1); return val; } @@ -250,24 +246,24 @@ class ValueRepresentation { } write(stream, type) { - var args = Array.from(arguments); + const args = Array.from(arguments); if (args[2] === null || args[2] === "" || args[2] === undefined) { return [stream.writeAsciiString("")]; } else { - var written = [], + const written = [], valueArgs = args.slice(2), func = stream["write" + type]; if (Array.isArray(valueArgs[0])) { if (valueArgs[0].length < 1) { written.push(0); } else { - var self = this; + const self = this; valueArgs[0].forEach(function (v, k) { if (self.allowMultiple() && k > 0) { stream.writeUint8(VM_DELIMITER); } - var singularArgs = [v].concat(valueArgs.slice(1)); - var byteCount = func.apply(stream, singularArgs); + const singularArgs = [v].concat(valueArgs.slice(1)); + const byteCount = func.apply(stream, singularArgs); written.push(byteCount); }); } @@ -285,13 +281,13 @@ class ValueRepresentation { writeOptions = { allowInvalidVRLength: false } ) { const { allowInvalidVRLength } = writeOptions; - var valid = true, - valarr = Array.isArray(value) ? value : [value], - total = 0; + let valid = true; + const valarr = Array.isArray(value) ? value : [value]; + let total = 0; - for (var i = 0; i < valarr.length; i++) { - var checkValue = valarr[i], - checklen = lengths[i], + for (let i = 0; i < valarr.length; i++) { + const checkValue = valarr[i]; + let checklen = lengths[i], isString = false, displaylen = checklen; if (checkValue === null || allowInvalidVRLength) { @@ -299,7 +295,7 @@ class ValueRepresentation { } else if (this.checkLength) { valid = this.checkLength(checkValue); } else if (this.maxCharLength) { - var check = this.maxCharLength; //, checklen = checkValue.length; + const check = this.maxCharLength; //, checklen = checkValue.length; valid = checkValue.length <= check; displaylen = checkValue.length; isString = true; @@ -308,7 +304,7 @@ class ValueRepresentation { } if (!valid) { - var errmsg = + const errmsg = "Value exceeds max length, vr: " + this.type + ", value: " + @@ -325,7 +321,7 @@ class ValueRepresentation { } //check for odd - var written = total; + let written = total; if (total & 1) { stream.writeUint8(this.padByte); written++; @@ -345,14 +341,14 @@ class ValueRepresentation { } static createByTypeString(type) { - var vr = VRinstances[type]; + let vr = VRinstances[type]; if (vr === undefined) { - if (type == "ox") { + if (type === "ox") { // TODO: determine VR based on context (could be 1 byte pixel data) // https://github.com/dgobbi/vtk-dicom/issues/38 validationLog.error("Invalid vr type", type, "- using OW"); vr = VRinstances["OW"]; - } else if (type == "xs") { + } else if (type === "xs") { validationLog.error("Invalid vr type", type, "- using US"); vr = VRinstances["US"]; } else { @@ -407,17 +403,17 @@ class BinaryRepresentation extends ValueRepresentation { } writeBytes(stream, value, _syntax, isEncapsulated, writeOptions = {}) { - var i; - var binaryStream; - var { fragmentMultiframe = true } = writeOptions; + let i; + let binaryStream; + let { fragmentMultiframe = true } = writeOptions; value = value === null || value === undefined ? [] : value; if (isEncapsulated) { - var fragmentSize = 1024 * 20, - frames = value.length, - startOffset = []; + const fragmentSize = 1024 * 20, + frames = value.length; + let startOffset = []; // Calculate a total length for storing binary stream - var bufferLength = 0; + let bufferLength = 0; for (i = 0; i < frames; i++) { const needsPadding = Boolean(value[i].byteLength & 1); bufferLength += value[i].byteLength + (needsPadding ? 1 : 0); @@ -431,36 +427,36 @@ class BinaryRepresentation extends ValueRepresentation { bufferLength += fragmentsLength * 8; } - binaryStream = new WriteBufferStream( - bufferLength, - stream.isLittleEndian - ); + binaryStream = new WriteBufferStream({ + defaultSize: bufferLength, + littleEndian: stream.isLittleEndian + }); for (i = 0; i < frames; i++) { const needsPadding = Boolean(value[i].byteLength & 1); startOffset.push(binaryStream.size); - var frameBuffer = value[i], + const frameBuffer = value[i], frameStream = new ReadBufferStream(frameBuffer); - var fragmentsLength = 1; + let fragmentsLength = 1; if (fragmentMultiframe) { fragmentsLength = Math.ceil( frameStream.size / fragmentSize ); } - for (var j = 0, fragmentStart = 0; j < fragmentsLength; j++) { + for (let j = 0, fragmentStart = 0; j < fragmentsLength; j++) { const isFinalFragment = j === fragmentsLength - 1; - var fragmentEnd = fragmentStart + frameStream.size; + let fragmentEnd = fragmentStart + frameStream.size; if (fragmentMultiframe) { fragmentEnd = fragmentStart + fragmentSize; } if (isFinalFragment) { fragmentEnd = frameStream.size; } - var fragStream = new ReadBufferStream( + const fragStream = new ReadBufferStream( frameStream.getBuffer(fragmentStart, fragmentEnd) ); fragmentStart = fragmentEnd; @@ -493,7 +489,7 @@ class BinaryRepresentation extends ValueRepresentation { return 0xffffffff; } else { - var binaryData = value[0]; + const binaryData = value[0]; binaryStream = new ReadBufferStream(binaryData); stream.concat(binaryStream); return super.writeBytes( @@ -506,18 +502,18 @@ class BinaryRepresentation extends ValueRepresentation { } readBytes(stream, length) { - if (length == 0xffffffff) { - var itemTagValue = Tag.readTag(stream), - frames = []; + if (length === 0xffffffff) { + const itemTagValue = Tag.readTag(stream); + let frames = []; if (itemTagValue.is(0xfffee000)) { - var itemLength = stream.readUint32(), - numOfFrames = 1, + const itemLength = stream.readUint32(); + let numOfFrames = 1, offsets = []; if (itemLength > 0x0) { //has frames numOfFrames = itemLength / 4; - var i = 0; + let i = 0; while (i++ < numOfFrames) { offsets.push(stream.readUint32()); } @@ -564,10 +560,10 @@ class BinaryRepresentation extends ValueRepresentation { // create a new readable stream based on the range const rangeStream = new ReadBufferStream( stream.buffer, - stream.isLittleEndian, { start: start, stop: stop, + littleEndian: stream.isLittleEndian, noCopy: stream.noCopy } ); @@ -623,7 +619,7 @@ class BinaryRepresentation extends ValueRepresentation { } return frames; } else { - var bytes; + let bytes; /*if (this.type == 'OW') { bytes = stream.readUint16Array(length); } else if (this.type == 'OB') { @@ -953,8 +949,8 @@ class PersonName extends EncodedStringRepresentation { } static checkComponentLengths(components) { - for (var i in components) { - var cmp = components[i]; + for (let i in components) { + const cmp = components[i]; // As per table 6.2-1 in the spec if (cmp.length > 64) return false; } @@ -966,6 +962,7 @@ class PersonName extends EncodedStringRepresentation { // style string, regardless of typeof value addValueAccessors(value) { if (typeof value === "string") { + // Ignore linter here and keep the new to properly convert into a string. value = new String(value); } if (value != undefined) { @@ -1003,7 +1000,7 @@ class PersonName extends EncodedStringRepresentation { // https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.4.html const values = value.split(String.fromCharCode(VM_DELIMITER)); - for (var pnString of values) { + for (let pnString of values) { const components = pnString.split( String.fromCharCode(PN_COMPONENT_DELIMITER) ); @@ -1089,41 +1086,41 @@ class SequenceOfItems extends ValueRepresentation { if (sqlength == 0x0) { return []; //contains no dataset } else { - var undefLength = sqlength == 0xffffffff, - elements = [], + const undefLength = sqlength === 0xffffffff; + let elements = [], read = 0; while (true) { - var tag = Tag.readTag(stream), - length = null; + const tag = Tag.readTag(stream); + let length = null; read += 4; if (tag.is(0xfffee0dd)) { stream.readUint32(); break; - } else if (!undefLength && read == sqlength) { + } else if (!undefLength && read === sqlength) { break; } else if (tag.is(0xfffee000)) { length = stream.readUint32(); read += 4; - var itemStream = null, - toRead = 0, - undef = length == 0xffffffff; + let itemStream = null, + toRead = 0; + const undef = length === 0xffffffff; if (undef) { - var stack = 0; + let stack = 0; /* eslint-disable-next-line no-constant-condition */ while (1) { - var g = stream.readUint16(); - if (g == 0xfffe) { + const g = stream.readUint16(); + if (g === 0xfffe) { // some control tag is about to be read - var ge = stream.readUint16(); + const ge = stream.readUint16(); let itemLength = stream.readUint32(); stream.increment(-4); - if (ge == 0xe00d) { + if (ge === 0xe00d) { if (itemLength === 0) { // item delimitation tag (0xfffee00d) + item length (0x00000000) has been read stack--; @@ -1140,11 +1137,11 @@ class SequenceOfItems extends ValueRepresentation { // anything else has been read toRead += 2; } - } else if (ge == 0xe000) { + } else if (ge === 0xe000) { // a new item has been found toRead += 4; - if (itemLength == 0xffffffff) { + if (itemLength === 0xffffffff) { // a new item with undefined length has been found stack++; } @@ -1168,10 +1165,10 @@ class SequenceOfItems extends ValueRepresentation { read += toRead; if (undef) stream.increment(8); - var items = DicomMessage._read(itemStream, syntax); + const items = DicomMessage._read(itemStream, syntax); elements.push(items); } - if (!undefLength && read == sqlength) { + if (!undefLength && read === sqlength) { break; } } @@ -1184,8 +1181,8 @@ class SequenceOfItems extends ValueRepresentation { let written = 0; if (value) { - for (var i = 0; i < value.length; i++) { - var item = value[i]; + for (let i = 0; i < value.length; i++) { + const item = value[i]; super.write(stream, "Uint16", 0xfffe); super.write(stream, "Uint16", 0xe000); super.write(stream, "Uint32", 0xffffffff); @@ -1457,14 +1454,14 @@ class ParsedUnknownValue extends BinaryRepresentation { read(stream, length, syntax, readOptions) { const arrayBuffer = this.readBytes(stream, length, syntax)[0]; - const streamFromBuffer = new ReadBufferStream(arrayBuffer, true); + const streamFromBuffer = new ReadBufferStream(arrayBuffer); const vr = ValueRepresentation.createByTypeString(this.type); if (vr.isBinary() && length > vr.maxLength && !vr.noMultiple) { - var values = []; - var rawValues = []; - var times = length / vr.maxLength, - i = 0; + const times = length / vr.maxLength; + let values = []; + let rawValues = []; + let i = 0; while (i++ < times) { const { rawValue, value } = vr.read( @@ -1554,4 +1551,8 @@ let VRinstances = { UV: new Unsigned64BitVeryLong() }; +ValueRepresentation.singleVRs = singleVRs; +ValueRepresentation.length32VRs = length32VRs; +ValueRepresentation.binaryVRs = binaryVRs; + export { ValueRepresentation }; diff --git a/src/adapters/Cornerstone/Segmentation_3X.js b/src/adapters/Cornerstone/Segmentation_3X.js index 2b87d4c8..be040436 100644 --- a/src/adapters/Cornerstone/Segmentation_3X.js +++ b/src/adapters/Cornerstone/Segmentation_3X.js @@ -1,4 +1,4 @@ -import log from "../../log.js"; +import log from "../../utilities/log.js"; import ndarray from "ndarray"; import { BitArray } from "../../bitArray.js"; import { datasetToBlob } from "../../datasetToBlob.js"; diff --git a/src/adapters/Cornerstone/Segmentation_4X.js b/src/adapters/Cornerstone/Segmentation_4X.js index 2ff8d858..f6a0c3f5 100644 --- a/src/adapters/Cornerstone/Segmentation_4X.js +++ b/src/adapters/Cornerstone/Segmentation_4X.js @@ -1,4 +1,4 @@ -import log from "../../log.js"; +import log from "../../utilities/log.js"; import ndarray from "ndarray"; import { BitArray } from "../../bitArray.js"; import { datasetToBlob } from "../../datasetToBlob.js"; diff --git a/src/bitArray.js b/src/bitArray.js index 9ba01cd9..8b7f0402 100644 --- a/src/bitArray.js +++ b/src/bitArray.js @@ -1,5 +1,5 @@ /* eslint no-bitwise: 0 */ -import log from "./log.js"; +import log from "./utilities/log.js"; const BitArray = { getBytesForBinaryFrame, diff --git a/src/constants/dicom.js b/src/constants/dicom.js index 8eb90f55..5922456b 100644 --- a/src/constants/dicom.js +++ b/src/constants/dicom.js @@ -100,43 +100,6 @@ export const TagHex = { MediaStorageSOPInstanceUID: "00020003" }; -export const encodingMapping = { - "": "iso-8859-1", - "iso-ir-6": "iso-8859-1", - "iso-ir-13": "shift-jis", - "iso-ir-100": "latin1", - "iso-ir-101": "iso-8859-2", - "iso-ir-109": "iso-8859-3", - "iso-ir-110": "iso-8859-4", - "iso-ir-126": "iso-ir-126", - "iso-ir-127": "iso-ir-127", - "iso-ir-138": "iso-ir-138", - "iso-ir-144": "iso-ir-144", - "iso-ir-148": "iso-ir-148", - "iso-ir-166": "tis-620", - "iso-2022-ir-6": "iso-8859-1", - "iso-2022-ir-13": "shift-jis", - "iso-2022-ir-87": "iso-2022-jp", - "iso-2022-ir-100": "latin1", - "iso-2022-ir-101": "iso-8859-2", - "iso-2022-ir-109": "iso-8859-3", - "iso-2022-ir-110": "iso-8859-4", - "iso-2022-ir-126": "iso-ir-126", - "iso-2022-ir-127": "iso-ir-127", - "iso-2022-ir-138": "iso-ir-138", - "iso-2022-ir-144": "iso-ir-144", - "iso-2022-ir-148": "iso-ir-148", - "iso-2022-ir-149": "euc-kr", - "iso-2022-ir-159": "iso-2022-jp", - "iso-2022-ir-166": "tis-620", - "iso-2022-ir-58": "iso-ir-58", - "iso-ir-192": "utf-8", - gb18030: "gb18030", - "iso-2022-gbk": "gbk", - "iso-2022-58": "gb2312", - gbk: "gbk" -}; - /** * Maps DICOM tag hex strings to their normalized lower camelCase names * for use in listener.information tracking @@ -243,3 +206,17 @@ export const BULKDATA_VRS = new Set([ "UT", // Unlimited Text "UV" // Unsigned 64-bit Very Long ]); + +export const binaryVRs = new Set(["FL", "FD", "SL", "SS", "UL", "US", "AT"]), + length32VRs = new Set([ + "OB", + "OW", + "OF", + "SQ", + "UC", + "UR", + "UT", + "UN", + "OD" + ]), + singleVRs = new Set(["SQ", "OF", "OW", "OB", "UN", "LT"]); diff --git a/src/constants/encodings.js b/src/constants/encodings.js new file mode 100644 index 00000000..a1ebc30e --- /dev/null +++ b/src/constants/encodings.js @@ -0,0 +1,39 @@ +export const encodingMapping = new Map([ + ["", "iso-8859-1"], + ["iso-ir-6", "iso-8859-1"], + ["iso-ir-13", "shift-jis"], + ["iso-ir-100", "iso-8859-1"], + ["iso-ir-101", "iso-8859-2"], + ["iso-ir-109", "iso-8859-3"], + ["iso-ir-110", "iso-8859-4"], + ["iso-ir-126", "iso-ir-126"], + ["iso-ir-127", "iso-ir-127"], + ["iso-ir-138", "iso-ir-138"], + ["iso-ir-144", "iso-ir-144"], + ["iso-ir-148", "iso-ir-148"], + ["iso-ir-166", "tis-620"], + ["iso-2022-ir-6", "iso-8859-1"], + ["iso-2022-ir-13", "shift-jis"], + ["iso-2022-ir-87", "iso-2022-jp"], + ["iso-2022-ir-100", "iso-8859-1"], + ["iso-2022-ir-101", "iso-8859-2"], + ["iso-2022-ir-109", "iso-8859-3"], + ["iso-2022-ir-110", "iso-8859-4"], + ["iso-2022-ir-126", "iso-ir-126"], + ["iso-2022-ir-127", "iso-ir-127"], + ["iso-2022-ir-138", "iso-ir-138"], + ["iso-2022-ir-144", "iso-ir-144"], + ["iso-2022-ir-148", "iso-ir-148"], + ["iso-2022-ir-149", "euc-kr"], + ["iso-2022-ir-159", "iso-2022-jp"], + ["iso-2022-ir-166", "tis-620"], + ["iso-2022-ir-58", "iso-ir-58"], + ["iso-ir-192", "utf-8"], + ["gb18030", "gb18030"], + ["iso-2022-gbk", "gbk"], + ["iso-2022-58", "gb2312"], + ["gbk", "gbk"] +]); + +export const defaultEncoding = "ascii"; +export const defaultDICOMEncoding = "iso-ir-192"; diff --git a/src/constants/sopClassUIDs.js b/src/constants/sopClassUIDs.js new file mode 100644 index 00000000..7d769ddc --- /dev/null +++ b/src/constants/sopClassUIDs.js @@ -0,0 +1,67 @@ +// Subset of those listed at, +// http,//dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_B.5 +export const sopClassNamesByUID = new Map([ + ["1.2.840.10008.5.1.4.1.1.20", "NMImage"], + ["1.2.840.10008.5.1.4.1.1.2", "CTImage"], + ["1.2.840.10008.5.1.4.1.1.2.1", "EnhancedCTImage"], + ["1.2.840.10008.5.1.4.1.1.2.2", "LegacyConvertedEnhancedCTImage"], + ["1.2.840.10008.5.1.4.1.1.3.1", "USMultiframeImage"], + ["1.2.840.10008.5.1.4.1.1.4", "MRImage"], + ["1.2.840.10008.5.1.4.1.1.4.1", "EnhancedMRImage"], + ["1.2.840.10008.5.1.4.1.1.4.2", "MRSpectroscopy"], + ["1.2.840.10008.5.1.4.1.1.4.3", "EnhancedMRColorImage"], + ["1.2.840.10008.5.1.4.1.1.4.4", "LegacyConvertedEnhancedMRImage"], + ["1.2.840.10008.5.1.4.1.1.6.1", "USImage"], + ["1.2.840.10008.5.1.4.1.1.6.2", "EnhancedUSVolume"], + ["1.2.840.10008.5.1.4.1.1.7", "SecondaryCaptureImage"], + ["1.2.840.10008.5.1.4.1.1.30", "ParametricMapStorage"], + ["1.2.840.10008.5.1.4.1.1.66", "RawData"], + ["1.2.840.10008.5.1.4.1.1.66.1", "SpatialRegistration"], + ["1.2.840.10008.5.1.4.1.1.66.2", "SpatialFiducials"], + ["1.2.840.10008.5.1.4.1.1.66.3", "DeformableSpatialRegistration"], + ["1.2.840.10008.5.1.4.1.1.66.4", "Segmentation"], + ["1.2.840.10008.5.1.4.1.1.66.7", "LabelmapSegmentation"], // Labelmap Segmentation SOP Class UID + ["1.2.840.10008.5.1.4.1.1.67", "RealWorldValueMapping"], + ["1.2.840.10008.5.1.4.1.1.88.11", "BasicTextSR"], + ["1.2.840.10008.5.1.4.1.1.88.22", "EnhancedSR"], + ["1.2.840.10008.5.1.4.1.1.88.33", "ComprehensiveSR"], + ["1.2.840.10008.5.1.4.1.1.88.34", "Comprehensive3DSR"], + ["1.2.840.10008.5.1.4.1.1.128", "PETImage"], + ["1.2.840.10008.5.1.4.1.1.130", "EnhancedPETImage"], + ["1.2.840.10008.5.1.4.1.1.128.1", "LegacyConvertedEnhancedPETImage"], + ["1.2.840.10008.5.1.4.1.1.77.1.5.1", "OphthalmicPhotography8BitImage"], + ["1.2.840.10008.5.1.4.1.1.77.1.5.4", "OphthalmicTomographyImage"] +]); + +export const sopClassUIDsByName = new Map([ + ["NMImage", "1.2.840.10008.5.1.4.1.1.20"], + ["CTImage", "1.2.840.10008.5.1.4.1.1.2"], + ["EnhancedCTImage", "1.2.840.10008.5.1.4.1.1.2.1"], + ["LegacyConvertedEnhancedCTImage", "1.2.840.10008.5.1.4.1.1.2.2"], + ["USMultiframeImage", "1.2.840.10008.5.1.4.1.1.3.1"], + ["MRImage", "1.2.840.10008.5.1.4.1.1.4"], + ["EnhancedMRImage", "1.2.840.10008.5.1.4.1.1.4.1"], + ["MRSpectroscopy", "1.2.840.10008.5.1.4.1.1.4.2"], + ["EnhancedMRColorImage", "1.2.840.10008.5.1.4.1.1.4.3"], + ["LegacyConvertedEnhancedMRImage", "1.2.840.10008.5.1.4.1.1.4.4"], + ["USImage", "1.2.840.10008.5.1.4.1.1.6.1"], + ["EnhancedUSVolume", "1.2.840.10008.5.1.4.1.1.6.2"], + ["SecondaryCaptureImage", "1.2.840.10008.5.1.4.1.1.7"], + ["ParametricMapStorage", "1.2.840.10008.5.1.4.1.1.30"], + ["RawData", "1.2.840.10008.5.1.4.1.1.66"], + ["SpatialRegistration", "1.2.840.10008.5.1.4.1.1.66.1"], + ["SpatialFiducials", "1.2.840.10008.5.1.4.1.1.66.2"], + ["DeformableSpatialRegistration", "1.2.840.10008.5.1.4.1.1.66.3"], + ["Segmentation", "1.2.840.10008.5.1.4.1.1.66.4"], + ["LabelmapSegmentation", "1.2.840.10008.5.1.4.1.1.66.7"], // Labelmap Segmentation SOP Class UID + ["RealWorldValueMapping", "1.2.840.10008.5.1.4.1.1.67"], + ["BasicTextSR", "1.2.840.10008.5.1.4.1.1.88.11"], + ["EnhancedSR", "1.2.840.10008.5.1.4.1.1.88.22"], + ["ComprehensiveSR", "1.2.840.10008.5.1.4.1.1.88.33"], + ["Comprehensive3DSR", "1.2.840.10008.5.1.4.1.1.88.34"], + ["PETImage", "1.2.840.10008.5.1.4.1.1.128"], + ["EnhancedPETImage", "1.2.840.10008.5.1.4.1.1.130"], + ["LegacyConvertedEnhancedPETImage", "1.2.840.10008.5.1.4.1.1.128.1"], + ["OphthalmicPhotography8BitImage", "1.2.840.10008.5.1.4.1.1.77.1.5.1"], + ["OphthalmicTomographyImage", "1.2.840.10008.5.1.4.1.1.77.1.5.4"] +]); diff --git a/src/constants/syntaxes.js b/src/constants/syntaxes.js new file mode 100644 index 00000000..620f877b --- /dev/null +++ b/src/constants/syntaxes.js @@ -0,0 +1,22 @@ +export const encapsulatedSyntaxes = new Set([ + "1.2.840.10008.1.2.4.50", + "1.2.840.10008.1.2.4.51", + "1.2.840.10008.1.2.4.57", + "1.2.840.10008.1.2.4.70", + "1.2.840.10008.1.2.4.80", + "1.2.840.10008.1.2.4.81", + "1.2.840.10008.1.2.4.90", + "1.2.840.10008.1.2.4.91", + "1.2.840.10008.1.2.4.92", + "1.2.840.10008.1.2.4.93", + "1.2.840.10008.1.2.4.94", + "1.2.840.10008.1.2.4.95", + "1.2.840.10008.1.2.5", + "1.2.840.10008.1.2.6.1", + "1.2.840.10008.1.2.4.100", + "1.2.840.10008.1.2.4.102", + "1.2.840.10008.1.2.4.103", + "1.2.840.10008.1.2.4.201", + "1.2.840.10008.1.2.4.202", + "1.2.840.10008.1.2.4.203" +]); diff --git a/src/dicomweb.js b/src/dicomweb.js index cea4638b..c760b38b 100644 --- a/src/dicomweb.js +++ b/src/dicomweb.js @@ -1,4 +1,4 @@ -import log from "./log.js"; +import log from "./utilities/log.js"; class DICOMWEB { /* diff --git a/src/index.js b/src/index.js index d017a7e6..9ba2819c 100644 --- a/src/index.js +++ b/src/index.js @@ -14,7 +14,7 @@ registerPrivatesModule(privateData); import { Tag } from "./Tag.js"; import { ValueRepresentation } from "./ValueRepresentation.js"; import { Colors } from "./colors.js"; -import log from "./log.js"; +import log from "./utilities/log.js"; import { AsyncDicomReader } from "./AsyncDicomReader.js"; diff --git a/src/normalizers.js b/src/normalizers.js index 1593c25f..8c602ef4 100644 --- a/src/normalizers.js +++ b/src/normalizers.js @@ -1,4 +1,4 @@ -import log from "./log.js"; +import log from "./utilities/log.js"; import { DicomMetaDictionary } from "./DicomMetaDictionary.js"; import { DerivedImage } from "./derivations/index.js"; import Segmentation from "./derivations/Segmentation.js"; @@ -35,45 +35,50 @@ class Normalizer { sopClassUID = sopClassUID.replace(/[^0-9.]/g, ""); // TODO: clean all VRs as part of normalizing let toUID = DicomMetaDictionary.sopClassUIDsByName; let sopClassUIDMap = {}; - sopClassUIDMap[toUID.NMImage] = NMImageNormalizer; - sopClassUIDMap[toUID.CTImage] = CTImageNormalizer; - sopClassUIDMap[toUID.ParametricMapStorage] = PMImageNormalizer; - sopClassUIDMap[toUID.MRImage] = MRImageNormalizer; - sopClassUIDMap[toUID.EnhancedCTImage] = EnhancedCTImageNormalizer; - sopClassUIDMap[toUID.LegacyConvertedEnhancedCTImage] = + sopClassUIDMap[toUID.get("NMImage")] = NMImageNormalizer; + sopClassUIDMap[toUID.get("CTImage")] = CTImageNormalizer; + sopClassUIDMap[toUID.get("ParametricMapStorage")] = PMImageNormalizer; + sopClassUIDMap[toUID.get("MRImage")] = MRImageNormalizer; + sopClassUIDMap[toUID.get("EnhancedCTImage")] = EnhancedCTImageNormalizer; - sopClassUIDMap[toUID.EnhancedMRImage] = EnhancedMRImageNormalizer; - sopClassUIDMap[toUID.LegacyConvertedEnhancedMRImage] = + sopClassUIDMap[toUID.get("LegacyConvertedEnhancedCTImage")] = + EnhancedCTImageNormalizer; + sopClassUIDMap[toUID.get("EnhancedMRImage")] = + EnhancedMRImageNormalizer; + sopClassUIDMap[toUID.get("LegacyConvertedEnhancedMRImage")] = EnhancedMRImageNormalizer; - sopClassUIDMap[toUID.EnhancedUSVolume] = EnhancedUSVolumeNormalizer; - sopClassUIDMap[toUID.PETImage] = PETImageNormalizer; - sopClassUIDMap[toUID.EnhancedPETImage] = PETImageNormalizer; - sopClassUIDMap[toUID.LegacyConvertedEnhancedPETImage] = + sopClassUIDMap[toUID.get("EnhancedUSVolume")] = + EnhancedUSVolumeNormalizer; + sopClassUIDMap[toUID.get("PETImage")] = PETImageNormalizer; + sopClassUIDMap[toUID.get("EnhancedPETImage")] = PETImageNormalizer; + sopClassUIDMap[toUID.get("LegacyConvertedEnhancedPETImage")] = PETImageNormalizer; - sopClassUIDMap[toUID.Segmentation] = SEGImageNormalizer; - sopClassUIDMap[toUID.DeformableSpatialRegistration] = DSRNormalizer; - sopClassUIDMap[toUID.OphthalmicPhotography8BitImage] = + sopClassUIDMap[toUID.get("Segmentation")] = SEGImageNormalizer; + sopClassUIDMap[toUID.get("DeformableSpatialRegistration")] = + DSRNormalizer; + sopClassUIDMap[toUID.get("OphthalmicPhotography8BitImage")] = OPImageNormalizer; - sopClassUIDMap[toUID.OphthalmicTomographyImage] = OCTImageNormalizer; - sopClassUIDMap[toUID.LabelmapSegmentation] = SEGImageNormalizer; // Labelmap Segmentation uses the same normalizer as Segmentation + sopClassUIDMap[toUID.get("OphthalmicTomographyImage")] = + OCTImageNormalizer; + sopClassUIDMap[toUID.get("LabelmapSegmentation")] = SEGImageNormalizer; // Labelmap Segmentation uses the same normalizer as Segmentation return sopClassUIDMap[sopClassUID]; } static isMultiframeSOPClassUID(sopClassUID) { const toUID = DicomMetaDictionary.sopClassUIDsByName; const multiframeSOPClasses = [ - toUID.NMImage, - toUID.EnhancedMRImage, - toUID.LegacyConvertedEnhancedMRImage, - toUID.EnhancedCTImage, - toUID.LegacyConvertedEnhancedCTImage, - toUID.EnhancedUSVolume, - toUID.EnhancedPETImage, - toUID.LegacyConvertedEnhancedPETImage, - toUID.Segmentation, - toUID.ParametricMapStorage, - toUID.OphthalmicTomographyImage, - toUID.LabelmapSegmentation // Labelmap Segmentation SOP Class UID + toUID.get("NMImage"), + toUID.get("EnhancedMRImage"), + toUID.get("LegacyConvertedEnhancedMRImage"), + toUID.get("EnhancedCTImage"), + toUID.get("LegacyConvertedEnhancedCTImage"), + toUID.get("EnhancedUSVolume"), + toUID.get("EnhancedPETImage"), + toUID.get("LegacyConvertedEnhancedPETImage"), + toUID.get("Segmentation"), + toUID.get("ParametricMapStorage"), + toUID.get("OphthalmicTomographyImage"), + toUID.get("LabelmapSegmentation") // Labelmap Segmentation SOP Class UID ]; return multiframeSOPClasses.indexOf(sopClassUID) !== -1; } diff --git a/src/utilities/TID300/unit2CodingValue.js b/src/utilities/TID300/unit2CodingValue.js index cfe1f791..25af59cf 100644 --- a/src/utilities/TID300/unit2CodingValue.js +++ b/src/utilities/TID300/unit2CodingValue.js @@ -1,4 +1,4 @@ -import log from "../../log.js"; +import log from "../log.js"; const knownUnits = [ // Standard UCUM units. diff --git a/src/utilities/compression/rleSingleSamplePerPixel.js b/src/utilities/compression/rleSingleSamplePerPixel.js index 35934712..58850264 100644 --- a/src/utilities/compression/rleSingleSamplePerPixel.js +++ b/src/utilities/compression/rleSingleSamplePerPixel.js @@ -1,4 +1,4 @@ -import log from "../../log.js"; +import log from "../log.js"; /** * Encodes a non-bitpacked frame which has one sample per pixel. diff --git a/src/log.js b/src/utilities/log.js similarity index 100% rename from src/log.js rename to src/utilities/log.js diff --git a/src/utilities/selectEncoding.js b/src/utilities/selectEncoding.js new file mode 100644 index 00000000..cc1322ac --- /dev/null +++ b/src/utilities/selectEncoding.js @@ -0,0 +1,60 @@ +import { + defaultDICOMEncoding, + defaultEncoding, + encodingMapping +} from "../constants/encodings"; +import { log } from "./log"; + +export function selectDICOMEncoding(values, ignoreErrors = false) { + if (!values || !Array.isArray(values)) { + return defaultDICOMEncoding; // default encoding + } + + switch (values.length) { + case 0: + return defaultDICOMEncoding; // default encoding + case 1: + return values[0]; + default: + if (!ignoreErrors) { + throw Error( + `Using multiple character sets is not supported: ${values}` + ); + } + + // Fallthrough to warn and select first encoding + log.warn( + "Using multiple character sets is not supported, proceeding with just the first character set", + values + ); + return values[0]; + } +} + +// Translates the DICOM specified encoding into a Web or native encoding target +// so we can use decoding APIs to correctly handle DICOM buffers. +export function selectNativeEncoding(dicomEncoding, ignoreErrors = false) { + if ( + !dicomEncoding || + typeof dicomEncoding !== "string" || + dicomEncoding.length === 0 + ) { + return defaultEncoding; + } + + // if we get something like "iso-ir-13\iso-ir-166", make sure we select "iso-ir-13". Unit tests already test for this. + const sanitizedEncoding = dicomEncoding.split("\\").at(0).trim(); + // if we get something like "ISO_IR 166", we sanitize to "iso-ir-166". Unit tests already test for this. + const coding = sanitizedEncoding.replace(/[_ ]/g, "-").toLowerCase(); + if (encodingMapping.has(coding)) { + return encodingMapping.get(coding); + } else if (ignoreErrors) { + log.warn( + `Unsupported character set: ${coding}, using default + character set ${defaultEncoding}` + ); + } else { + throw Error(`Unsupported character set: ${coding}`); + } + return defaultEncoding; +} diff --git a/test/anonymizer.test.js b/test/anonymizer.test.js index 3215170b..255d8de9 100644 --- a/test/anonymizer.test.js +++ b/test/anonymizer.test.js @@ -1,6 +1,6 @@ import dcmjs from "../src/index.js"; import fs from "fs"; -import { validationLog } from "./../src/log.js"; +import { validationLog } from "../src/utilities/log.js"; // Ignore validation errors validationLog.setLevel(5); diff --git a/test/async-data.test.js b/test/async-data.test.js index cee1fab3..99e16b0d 100644 --- a/test/async-data.test.js +++ b/test/async-data.test.js @@ -343,7 +343,10 @@ describe("AsyncDicomReader", () => { // Append compressed pixel data with fragments const WriteBufferStream = dcmjs.data.WriteBufferStream; - const writeStream = new WriteBufferStream(null, true); + const writeStream = new WriteBufferStream({ + defaultSize: null, + littleEndian: true + }); // Write the base buffer first const baseArray = new Uint8Array(baseBuffer); diff --git a/test/data-encoding.test.js b/test/data-encoding.test.js index 51c87bab..ff4f9b79 100644 --- a/test/data-encoding.test.js +++ b/test/data-encoding.test.js @@ -3,9 +3,27 @@ import dcmjs from "../src/index.js"; import fs from "fs"; import fsPromises from "fs/promises"; import path from "path"; +import { defaultDICOMEncoding } from "../src/constants/encodings"; +import { selectDICOMEncoding } from "../src/utilities/selectEncoding"; const { DicomMetaDictionary, DicomMessage } = dcmjs.data; +const testEncodingItems = ["utf8", "multiple", "one", "none"]; + +const testEncodings = { + utf8: [defaultDICOMEncoding], + multiple: ["ISO_IR 13", "ISO_IR 166"], + one: ["ISO_IR 6"], + none: [] +}; + +const expectedEncodings = { + utf8: defaultDICOMEncoding, + multiple: "ISO_IR 13", + one: "ISO_IR 6", + none: defaultDICOMEncoding +}; + const expectedPatientNames = { SCSARAB: "قباني^لنزار", SCSFREN: "Buc^Jérôme", @@ -21,6 +39,14 @@ const expectedPatientNames = { //"SCSI2": "X", }; +it("test_encoding_selection", async () => { + testEncodingItems.forEach(item => { + const encoding = selectDICOMEncoding(testEncodings[item], true); + const expected = expectedEncodings[item]; + expect(encoding).toEqual(expected); + }); +}); + it("test_encodings", async () => { const url = "https://github.com/dcmjs-org/data/releases/download/dclunie-charsets/dclunie-charsets.zip"; @@ -53,7 +79,9 @@ it("test_encodings", async () => { expect(String(newDataset.PatientName)).toEqual( expectedPatientNames[fileName] ); - expect(newDataset.SpecificCharacterSet).toEqual("ISO_IR 192"); + expect(newDataset.SpecificCharacterSet).toEqual( + defaultDICOMEncoding + ); } }); }); diff --git a/test/data.test.js b/test/data.test.js index 3211f2cf..2966dcab 100644 --- a/test/data.test.js +++ b/test/data.test.js @@ -4,7 +4,7 @@ import fsPromises from "fs/promises"; import path from "path"; import { WriteBufferStream } from "../src/BufferStream"; import dcmjs from "../src/index.js"; -import { log } from "./../src/log.js"; +import { log } from "../src/utilities/log.js"; import { getTestDataset, getZippedTestDataset } from "./testUtils.js"; import { promisify } from "util"; @@ -734,7 +734,7 @@ it("Writes encapsulated OB data which has an odd length with a padding byte in i _vrMap: { PixelData: "OB" } }); - const stream = new WriteBufferStream(1024); + const stream = new WriteBufferStream({ defaultSize: 1024 }); const bytesWritten = DicomMessage.write( dataset, stream, @@ -875,10 +875,11 @@ describe("With a SpecificCharacterSet tag", () => { // - 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; + const stream = new WriteBufferStream({ + defaultSize: 16 + specificCharacterSet.length + encodedBytes.length + }); + stream.setEncoding(specificCharacterSet, readOptions.ignoreErrors); + stream.setLittleEndian(); // Write SpecificCharacterSet tag stream.writeUint32(0x00050008); diff --git a/test/defined-length-sequence.test.js b/test/defined-length-sequence.test.js index 2ed773a6..4800f576 100644 --- a/test/defined-length-sequence.test.js +++ b/test/defined-length-sequence.test.js @@ -22,7 +22,10 @@ function buildDefinedLengthSQBuffer(itemCodeValues, opts = {}) { const { undefinedSeqLength = false, undefinedItemLengths = false } = opts; // --- Meta header --- - const metaBody = new WriteBufferStream(256, true); + const metaBody = new WriteBufferStream({ + defaultSize: 256, + littleEndian: true + }); DicomMessage.writeTagObject( metaBody, TagHex.TransferSyntaxUID, @@ -33,10 +36,16 @@ function buildDefinedLengthSQBuffer(itemCodeValues, opts = {}) { ); // --- Sequence body: item headers + item content --- - const seqBody = new WriteBufferStream(4096, true); + const seqBody = new WriteBufferStream({ + defaultSize: 4096, + littleEndian: true + }); for (const val of itemCodeValues) { // Write item content using the standard writer - const itemBody = new WriteBufferStream(256, true); + const itemBody = new WriteBufferStream({ + defaultSize: 256, + littleEndian: true + }); DicomMessage.writeTagObject( itemBody, "00080100", @@ -69,7 +78,10 @@ function buildDefinedLengthSQBuffer(itemCodeValues, opts = {}) { } // --- Assemble Part 10 --- - const file = new WriteBufferStream(8192, true); + const file = new WriteBufferStream({ + defaultSize: 8192, + littleEndian: true + }); file.writeUint8Repeat(0, 128); file.writeAsciiString("DICM"); DicomMessage.writeTagObject( diff --git a/test/helper/DicomDataReadBufferStreamBuilder.js b/test/helper/DicomDataReadBufferStreamBuilder.js index faae1ecc..6a186545 100644 --- a/test/helper/DicomDataReadBufferStreamBuilder.js +++ b/test/helper/DicomDataReadBufferStreamBuilder.js @@ -82,8 +82,9 @@ export class DicomDataReadBufferStreamBuilder { build() { const byteArray = new Uint8Array(this.itemArray); - return new ReadBufferStream(byteArray.buffer, null, { - noCopy: this.options.noCopy + return new ReadBufferStream(byteArray.buffer, { + noCopy: this.options.noCopy, + littleEndian: false }); } } diff --git a/test/helper/sampleDicomPart10.js b/test/helper/sampleDicomPart10.js index c8c6287f..5a06a623 100644 --- a/test/helper/sampleDicomPart10.js +++ b/test/helper/sampleDicomPart10.js @@ -125,7 +125,10 @@ export function createSampleDicom(updates = {}, writeOptions = {}) { const useCustomPixelData = pixelData !== undefined || pixelDataLength !== undefined; - const metaStream = new WriteBufferStream(1024, true); + const metaStream = new WriteBufferStream({ + defaultSize: 1024, + littleEndian: true + }); if (!meta[TagHex.TransferSyntaxUID]) { meta[TagHex.TransferSyntaxUID] = { vr: "UI", @@ -136,7 +139,10 @@ export function createSampleDicom(updates = {}, writeOptions = {}) { allowInvalidVRLength: false }); - const fileStream = new WriteBufferStream(8192, true); + const fileStream = new WriteBufferStream({ + defaultSize: 8192, + littleEndian: true + }); fileStream.writeUint8Repeat(0, 128); fileStream.writeAsciiString("DICM"); DicomMessage.writeTagObject( diff --git a/test/integration/DicomMessage.readFile.test.js b/test/integration/DicomMessage.readFile.test.js index 93241b08..e650974a 100644 --- a/test/integration/DicomMessage.readFile.test.js +++ b/test/integration/DicomMessage.readFile.test.js @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import dcmjs from "../../src/index.js"; -import { validationLog } from "./../../src/log.js"; +import { validationLog } from "../../src/utilities/log.js"; // Ignore validation errors validationLog.setLevel(5); diff --git a/test/lossless-read-write.test.js b/test/lossless-read-write.test.js index edb84e8e..9fdff554 100644 --- a/test/lossless-read-write.test.js +++ b/test/lossless-read-write.test.js @@ -34,7 +34,7 @@ const EXPLICIT_VR_LENGTH32 = [ * Supports both Implicit and Explicit VR Little Endian for the dataset. * * @param {ArrayBuffer|Uint8Array} buffer - Raw DICOM file buffer - * @param {string} [transferSyntaxUID] - Optional. If provided, used to decide Implicit vs Explicit VR (e.g. from meta); avoids parsing meta. + * @param {string} [transferSyntaxUIDHint] - Optional. If provided, used to decide Implicit vs Explicit VR (e.g. from meta); avoids parsing meta. * @returns {{ length: number, data: ArrayBuffer } | null} - PixelData length and bytes, or null if not found */ function readPixelDataFromRawBuffer(buffer, transferSyntaxUIDHint) { @@ -295,6 +295,111 @@ describe("lossless-read-write", () => { } }; + const resultDataset = { + "00080008": { + vr: "CS", + Value: ["DERIVED"], + _rawValue: ["DERIVED "] + }, + "00082112": { + vr: "SQ", + Value: [ + { + "00081150": { + vr: "UI", + Value: ["1.2.840.10008.5.1.4.1.1.7"], + _rawValue: ["1.2.840.10008.5.1.4.1.1.7"] + } + } + ], + _rawValue: undefined + }, + "00180050": { + vr: "DS", + Value: [1], + _rawValue: ["1 "] + }, + "00181708": { + vr: "IS", + Value: [426], + _rawValue: ["426 "] + }, + "00189328": { + vr: "FD", + Value: [30.98], + _rawValue: [30.98] + }, + "0020000D": { + vr: "UI", + Value: [ + "1.3.6.1.4.1.5962.99.1.2280943358.716200484.1363785608958.3.0" + ], + _rawValue: [ + "1.3.6.1.4.1.5962.99.1.2280943358.716200484.1363785608958.3.0" + ] + }, + "00400254": { + vr: "LO", + Value: ["DUCTO/GALACTOGRAM 1 DUCT LT"], + _rawValue: ["DUCTO/GALACTOGRAM 1 DUCT LT "] + }, + "7FE00010": { + vr: "OW", + Value: [new Uint8Array([0x00, 0x00]).buffer], + _rawValue: undefined + } + }; + + const metaResult = { + "00020010": { + Value: ["1.2.840.10008.1.2.1"], + _rawValue: ["1.2.840.10008.1.2.1"], + vr: "UI" + } + }; + + const testEncodings = [ + "ISO_IR 6", + "ISO_IR 13", + "ISO_IR 166", + "iso-ir-100", + "iso-ir-192" + ]; + + test("Ignore _rawValue in write and read", () => { + const dicomDict = new DicomDict({}); + dicomDict.dict = dataset; + + // write and re-read + const writtenBuffer = dicomDict.write(); + const outputDicomDict = DicomMessage.readFile(writtenBuffer); + + const resultDicomDict = new DicomDict({}); + resultDicomDict.meta = metaResult; + resultDicomDict.dict = resultDataset; + + expect(outputDicomDict).toEqual(resultDicomDict); + }); + + test("Basic encoded write and read", () => { + testEncodings.forEach(encoding => { + const dicomDict = new DicomDict({}); + dicomDict.dict = dataset; + + // write and re-read + const writtenBuffer = dicomDict.write({ + encoding + }); + const outputDicomDict = DicomMessage.readFile(writtenBuffer); + + const resultDicomDict = new DicomDict({}); + resultDicomDict.meta = metaResult; + resultDicomDict.dict = resultDataset; + + expect(outputDicomDict).toEqual(resultDicomDict); + }); + }); + test("storeRaw flag on VR should be respected by read", () => { const tagsWithoutRaw = ["00082112", "7FE00010"]; diff --git a/test/readBufferStream.test.js b/test/readBufferStream.test.js index 6f44bf87..aed337ff 100644 --- a/test/readBufferStream.test.js +++ b/test/readBufferStream.test.js @@ -49,9 +49,10 @@ describe("ReadBufferStream Tests", () => { describe("substream", () => { it("gets range of buffer", () => { - const stream = new ReadBufferStream(buffer, false, { + const stream = new ReadBufferStream(buffer, { start: 32, - stop: 64 + stop: 64, + littleEndian: false }); expect(stream.available).toBe(32); expect(stream.startOffset).toBe(32); @@ -63,33 +64,34 @@ describe("ReadBufferStream Tests", () => { }); it("creates subranges on buffer", () => { - const stream = new ReadBufferStream(buffer, false, { + const stream = new ReadBufferStream(buffer, { start: 32, - stop: 64 + stop: 64, + littleEndian: false + }); + const subStream = new ReadBufferStream(stream.buffer, { + start: stream.offset, + stop: stream.size, + littleEndian: stream.isLittleEndian }); - const subStream = new ReadBufferStream( - stream.buffer, - stream.isLittleEndian, - { start: stream.offset, stop: stream.size } - ); expect(subStream.startOffset).toBe(32); expect(subStream.endOffset).toBe(64); expect(subStream.size).toBe(64); }); it("creates subranges on stream", () => { - const stream = new ReadBufferStream(buffer, false, { + const stream = new ReadBufferStream(buffer, { start: 32, - stop: 64 + stop: 64, + littleEndian: false }); // This is the recommended way of creating // a sub-stream as it allows either copying // or referencing the incoming stream data. - const subStream = new ReadBufferStream( - stream, - stream.isLittleEndian, - { stop: 48 } - ); + const subStream = new ReadBufferStream(stream, { + stop: 48, + littleEndian: stream.isLittleEndian + }); expect(subStream.available).toBe(16); expect(subStream.readUint8()).toBe(32); }); @@ -97,8 +99,9 @@ describe("ReadBufferStream Tests", () => { describe("isAvailable", () => { it("determines when data is correctly available", () => { - const stream = new ReadBufferStream(null, false, { - clearBuffers: true + const stream = new ReadBufferStream(null, { + clearBuffers: true, + littleEndian: false }); expect(stream.isAvailable(0)).toBe(true); expect(stream.isAvailable(1)).toBe(false); diff --git a/test/testUtils.js b/test/testUtils.js index 2d5c1337..4720ea39 100644 --- a/test/testUtils.js +++ b/test/testUtils.js @@ -3,7 +3,7 @@ import os from "os"; import path from "path"; import followRedirects from "follow-redirects"; import AdmZip from "adm-zip"; -import { validationLog } from "./../src/log.js"; +import { validationLog } from "../src/utilities/log.js"; const { https } = followRedirects; diff --git a/test/writeBufferStream.test.js b/test/writeBufferStream.test.js index 83d03e44..3cfe98ea 100644 --- a/test/writeBufferStream.test.js +++ b/test/writeBufferStream.test.js @@ -2,7 +2,10 @@ import { ReadBufferStream, WriteBufferStream } from "../src/BufferStream"; describe("WriteBufferStream Tests", () => { it("writeUint8", () => { - const stream = new WriteBufferStream(25, true); + const stream = new WriteBufferStream({ + defaultSize: 25, + littleEndian: true + }); expect(stream).toBeDefined(); for (let i = 0; i < 512; i++) { stream.writeUint8(i % 256); @@ -24,7 +27,10 @@ describe("WriteBufferStream Tests", () => { }); it("writeUint16", () => { - const stream = new WriteBufferStream(25, true); + const stream = new WriteBufferStream({ + defaultSize: 25, + littleEndian: true + }); expect(stream).toBeDefined(); for (let i = 0; i < 512; i++) { stream.writeUint16((i * 511) % 0x10000); @@ -37,7 +43,10 @@ describe("WriteBufferStream Tests", () => { }); it("writeUint32", () => { - const stream = new WriteBufferStream(25, true); + const stream = new WriteBufferStream({ + defaultSize: 25, + littleEndian: true + }); expect(stream).toBeDefined(); const expected = []; for (let i = 0; i < 512; i++) { @@ -70,7 +79,7 @@ describe("WriteBufferStream Tests", () => { }); it("writesLongStrings", () => { - const stream = new WriteBufferStream(32, true); + const stream = new WriteBufferStream({ defaultSize: 32 }); let string = "0"; for (let i = 1; i < 512; i++) { string = string + ", " + i; @@ -80,7 +89,7 @@ describe("WriteBufferStream Tests", () => { }); describe("readWorksAfterWrite", () => { - const out = new WriteBufferStream(3, true); + const out = new WriteBufferStream({ defaultSize: 3 }); const testStr = "Hello World"; // 64 bits out.writeUint8Repeat(1, 128); @@ -97,7 +106,12 @@ describe("WriteBufferStream Tests", () => { out.writeUint16(234); out.writeUint8(25); const firstSize = out.size; - out.concat(new ReadBufferStream(out, out.isLittleEndian, { start: 0 })); + out.concat( + new ReadBufferStream(out, { + start: 0, + littleEndian: out.isLittleEndian + }) + ); expect(out.size).toBe(firstSize * 2); const checkValues = stream => { @@ -129,8 +143,9 @@ describe("WriteBufferStream Tests", () => { }); it("Should clone with stream", () => { - const stream = new ReadBufferStream(out, out.isLittleEndian, { - start: 0 + const stream = new ReadBufferStream(out, { + start: 0, + littleEndian: out.isLittleEndian }); expect(stream.size).toBe(out.size); checkValues(stream); @@ -140,13 +155,10 @@ describe("WriteBufferStream Tests", () => { }); it("Should clone with buffer", () => { - const stream = new ReadBufferStream( - out.buffer, - out.isLittleEndian, - { - stop: out.size - } - ); + const stream = new ReadBufferStream(out.buffer, { + stop: out.size, + littleEndian: out.isLittleEndian + }); expect(stream.size).toBe(out.size); checkValues(stream); // Second copy identical @@ -155,10 +167,9 @@ describe("WriteBufferStream Tests", () => { }); it("Should clone with slice", () => { - const stream = new ReadBufferStream( - out.slice(0, out.size), - out.isLittleEndian - ); + const stream = new ReadBufferStream(out.slice(0, out.size), { + littleEndian: out.isLittleEndian + }); expect(stream.size).toBe(out.size); checkValues(stream); // Second copy identical