Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions src/BufferStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,40 @@ class BufferStream {
size = 0;
view = new SplitDataView();

/** Indicates if this buffer stream is complete/has finished being created */
isComplete = false;

/** A flag to set to indicate to clear buffers as they get consumed */
clearBuffers = false;

encoder = new TextEncoder("utf-8");

constructor(options = null) {
this.isLittleEndian = options?.littleEndian || this.isLittleEndian;
this.view.defaultSize = options?.defaultSize ?? this.view.defaultSize;
this.clearBuffers = options.clearBuffers || false;
}

/**
* Mark this stream as having finished being written or read from
*/
setComplete(value = true) {
this.isComplete = value;
}

/**
* Indicates if the value length is currently available in the already
* read/defined portion of the stream
*/
isAvailable(length) {
// console.warn(
// "isAvailable",
// length,
// this.offset,
// this.size,
// this.endOffset
// );
return this.offset + length <= this.endOffset;
}

setEndian(isLittle) {
Expand All @@ -37,6 +66,11 @@ class BufferStream {
return this.view.slice(start, end);
}

/**
* @deprecated Gets the entire buffer at once. Suggest using the
* view instead, and writing an iterator over the parts to finish
* writing it.
*/
getBuffer(start = 0, end = this.size) {
if (this.noCopy) {
return new Uint8Array(this.slice(start, end));
Expand Down Expand Up @@ -272,6 +306,7 @@ class BufferStream {
);
this.offset += stream.size;
this.size = this.offset;
this.endOffset = this.size;
return this.view.availableSize;
}

Expand All @@ -291,13 +326,40 @@ class BufferStream {
* @param {*} options.start for the start of the new buffer to use
* @param {*} options.end for the end of the buffer to use
* @param {*} options.transfer to transfer the buffer to be owned
* Transfer will default true if the entire buffer is being added.
* It should be set explicitly to false to NOT transfer.
*/
addBuffer(buffer, options = null) {
if (!buffer) {
// Silently ignore null buffers.
return;
}
this.view.addBuffer(buffer, options);
this.size = this.view.size;
this.endOffset = this.size;
return this.size;
}

/**
* Consumes the data up to the given offset.
* This will clear the references to the data buffers, and will
* cause resets etc to fail.
* The default offset is the current position, so everything already read.
*/
consume(offset = this.offset) {
if (!this.clearBuffers) {
return;
}
this.view.consume(offset);
}

/**
* Returns true if the stream has data in the given range.
*/
hasData(start, end) {
return this.view.hasData(start, end);
}

more(length) {
if (this.offset + length > this.endOffset) {
throw new Error("Request more than currently allocated buffer");
Expand All @@ -313,6 +375,7 @@ class BufferStream {
this.slice(this.offset, this.offset + length)
);
this.increment(length);
newBuf.setComplete();

return newBuf;
}
Expand All @@ -323,7 +386,7 @@ class BufferStream {
}

end() {
return this.offset >= this.view.byteLength;
return this.isComplete && this.offset >= this.end;
}

toEnd() {
Expand All @@ -341,14 +404,16 @@ class ReadBufferStream extends BufferStream {
noCopy: false
}
) {
super({ littleEndian });
super({ ...options, littleEndian });
this.noCopy = options.noCopy;
this.decoder = new TextDecoder("latin1");

if (buffer instanceof BufferStream) {
this.view.from(buffer.view, options);
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;
Expand All @@ -367,7 +432,7 @@ class ReadBufferStream extends BufferStream {
}

end() {
return this.offset >= this.endOffset;
return this.isComplete && this.offset >= this.endOffset;
}

toEnd() {
Expand Down
182 changes: 142 additions & 40 deletions src/DicomMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,82 @@ class DicomMessage {
});
}

/**
* Checks if the FMI has been read successfully, returning the
* main syntax if it has, otherwise reads the file meta information
* starting at the current offset.
*/
static _readFmi(dictCreator, stream) {
if (dictCreator.fmi) {
return dictCreator.mainSyntax;
}

if (!stream.isAvailable(16) && !stream.isComplete) {
return false;
}
const useSyntax = EXPLICIT_LITTLE_ENDIAN;
// read the first tag to check if it's the meta length tag
const el = DicomMessage._readTag(stream, useSyntax);

let metaHeader;
if (el.tag.toCleanString() !== "00020000") {
// meta length tag is missing
if (!dictCreator.options.ignoreErrors) {
throw new Error(
"Invalid DICOM file, meta length tag is malformed or not present."
);
}

// reset stream to the position where we started reading tags
stream.offset = dictCreator.metaStartPos;

if (!stream.isAvailable(10240) && !stream.isComplete) {
// Not enough data available yet, wait for at least 10k
// to start reading.
return false;
}

// read meta header elements sequentially
metaHeader = DicomMessage._read(stream, useSyntax, {
untilTag: "00030000",
stopOnGreaterTag: true,
ignoreErrors: true
});
if (!metaHeader) {
stream.offset = dictCreator.metaStartPos;
return false;
}
} else {
// meta length tag is present
var metaLength = el.values[0];

if (!stream.isAvailable(metaLength) && !stream.isComplete) {
stream.offset = dictCreator.metaStartPos;
return false;
}
// read header buffer using the specified meta length
var metaStream = stream.more(metaLength);
// Use a new options object to read without the custom options
// applying to the FMI
metaHeader = DicomMessage._read(metaStream, useSyntax, {});
}

dictCreator.fmi = metaHeader;
stream.consume();

//get the syntax
const mainSyntax = metaHeader["00020010"].Value[0];

if (!mainSyntax) {
throw new Error(
`No syntax provided in ${JSON.stringify(metaHeader)}`
);
}
dictCreator.mainSyntax = mainSyntax;

return mainSyntax;
}

static _read(
bufferStream,
syntax,
Expand All @@ -126,6 +202,12 @@ class DicomMessage {
try {
let previousTagOffset;
while (!bufferStream.end()) {
if (
!bufferStream.isAvailable(1024) &&
!bufferStream.isComplete
) {
return false;
}
if (dictCreator.continueParse(bufferStream)) {
continue;
}
Expand Down Expand Up @@ -217,6 +299,26 @@ class DicomMessage {
return encapsulatedSyntaxes.indexOf(syntax) != -1;
}

/**
* If there is enough available data, read the 128 byte prefix and the
* DICM header marker, returning true when done, throwing an error when
* not a DICM stream, and returning false if not yet enough data.
*/
static _readDICM(dictCreator, stream) {
if (!stream.isAvailable(132) && !stream.isComplete) {
return false;
}
stream.reset();
stream.increment(128);
if (stream.readAsciiString(4) !== "DICM") {
throw new Error("Invalid DICOM file, expected header is missing");
}
dictCreator.metaStartPos = stream.offset;
stream.consume();

return true;
}

/**
* Reads a DICOM input stream from an array buffer.
*
Expand All @@ -233,65 +335,65 @@ class DicomMessage {
forceStoreRaw: false
}
) {
var stream = new ReadBufferStream(buffer, null, {
noCopy: options.noCopy
}),
useSyntax = EXPLICIT_LITTLE_ENDIAN;
stream.reset();
stream.increment(128);
if (stream.readAsciiString(4) !== "DICM") {
throw new Error("Invalid DICOM file, expected header is missing");
if (!options.dictCreator) {
options.dictCreator = new DictCreator(this, options);
}
const { dictCreator } = options;
if (options.stream === true) {
// Create a streaming input buffer
options.stream = new ReadBufferStream(null, true, {
clearBuffers: options.clearBuffers ?? true
});
}

// save position before reading first tag
var metaStartPos = stream.offset;

// read the first tag to check if it's the meta length tag
var el = DicomMessage._readTag(stream, useSyntax);
let stream =
options.stream ||
new ReadBufferStream(buffer, null, {
noCopy: options.noCopy
});
if (options.stream) {
stream.addBuffer(buffer);
}

var metaHeader = {};
if (el.tag.toCleanString() !== "00020000") {
// meta length tag is missing
if (!options.ignoreErrors) {
throw new Error(
"Invalid DICOM file, meta length tag is malformed or not present."
);
if (dictCreator.metaStartPos === -1) {
const dicmResult = this._readDICM(dictCreator, stream);
if (!dicmResult) {
return false;
}
}

// reset stream to the position where we started reading tags
stream.offset = metaStartPos;

// read meta header elements sequentially
metaHeader = DicomMessage._read(stream, useSyntax, {
untilTag: "00030000",
stopOnGreaterTag: true,
ignoreErrors: true
});
} else {
// meta length tag is present
var metaLength = el.values[0];

// read header buffer using the specified meta length
var metaStream = stream.more(metaLength);
metaHeader = DicomMessage._read(metaStream, useSyntax, options);
if (!dictCreator.mainSyntax) {
const result = this._readFmi(dictCreator, stream);
if (!result) {
return false;
}
}

//get the syntax
var mainSyntax = metaHeader["00020010"].Value[0];
let { mainSyntax } = dictCreator;

//in case of deflated dataset, decompress and continue
if (mainSyntax === DEFLATED_EXPLICIT_LITTLE_ENDIAN) {
if (!stream.isComplete) {
return false;
}
stream = new DeflatedReadBufferStream(stream, {
noCopy: options.noCopy
});
}

mainSyntax = DicomMessage._normalizeSyntax(mainSyntax);
var objects = DicomMessage._read(stream, mainSyntax, options);
const objects = DicomMessage._read(stream, mainSyntax, options);
if (!objects) {
// Needs more data still
return false;
}

var dicomDict = new DicomDict(metaHeader);
var dicomDict = new DicomDict(dictCreator.fmi);
dicomDict.dict = objects;

// Reset, ready for another read
dictCreator.reset();

return dicomDict;
}

Expand Down
Loading