@cosyte/dicom 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/index.cjs +71 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.mjs +71 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d04ae1a: `serializeDicom()` now refuses a Private Creator or File Meta element whose value is over 65,534 bytes with `DicomSerializeError` code `VALUE_TOO_LONG_FOR_VR`, instead of throwing a `RangeError`.
|
|
8
|
+
|
|
9
|
+
PS3.5 section 6.2.2 has a value that long, in a VR with a 16-bit Value Length Field, written as `UN`, which the writer has done since 0.1.5, but forbids `UN` for a Private Creator Data Element (gggg,0010-00FF) and for any File Meta Information element (0002,xxxx). Such a value has no encoding, and the writer still put its length through the 16-bit write. Callers now see a `DicomSerializeError` they can narrow on `err.code === "VALUE_TOO_LONG_FOR_VR"`, with a fixed message that carries no tag, length or byte of the value, and nothing is written. It applies to a Private Creator under the Explicit VR syntaxes (Implicit VR Little Endian has a 32-bit length and writes it) and to a File Meta element under every syntax, since the File Meta group is always Explicit VR Little Endian. `deidentify()` throws the same error when it re-encodes a Sequence Item that keeps such a creator, which only a `RetainSafePrivate` run whose `Profile` names a creator that long does. `SERIALIZE_ERROR_CODES` gains the new code, so an exhaustive `switch` over `SerializeErrorCode` needs one more case.
|
|
10
|
+
|
|
11
|
+
## 0.1.6
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 39ef9b3: `deidentify()` and `Dataset.directory` now strip a value's padding in linear time, so a long run of NULL or SPACE bytes in a file no longer stalls them for seconds.
|
|
16
|
+
|
|
17
|
+
Seven trims of file-controlled text used patterns that backtracked quadratically on a run of pad bytes ending in any other byte: the NULL trim of every UID `deidentify()` remaps, its reads of a Private Creator, a Block Identifying Information Status and the Burned In Annotation flag, and the DICOMDIR's reads of its `CS` values. A SOP Instance UID of 50,000 NULL bytes and one letter took `deidentify()` close to four seconds, and the time grew with the square of the run. Each trim now scans the value once from its end, or from each end, and returns the same result as before.
|
|
18
|
+
|
|
3
19
|
## 0.1.5
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -11,7 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/version.ts
|
|
14
|
-
var VERSION = "0.1.
|
|
14
|
+
var VERSION = "0.1.7";
|
|
15
15
|
|
|
16
16
|
// src/dictionary/index.ts
|
|
17
17
|
var dictionary_exports = {};
|
|
@@ -11141,8 +11141,24 @@ function directoryRecords(source) {
|
|
|
11141
11141
|
}
|
|
11142
11142
|
function csValues(el) {
|
|
11143
11143
|
if (el === void 0) return void 0;
|
|
11144
|
-
const text = el.rawBytes.toString("latin1")
|
|
11145
|
-
return Object.freeze(text === "" ? [] : text.split("\\").map(
|
|
11144
|
+
const text = stripTrailingPad(el.rawBytes.toString("latin1"));
|
|
11145
|
+
return Object.freeze(text === "" ? [] : text.split("\\").map(trimSpaces));
|
|
11146
|
+
}
|
|
11147
|
+
function stripTrailingPad(text) {
|
|
11148
|
+
let end = text.length;
|
|
11149
|
+
while (end > 0) {
|
|
11150
|
+
const code = text.charCodeAt(end - 1);
|
|
11151
|
+
if (code !== 0 && code !== 32) break;
|
|
11152
|
+
end--;
|
|
11153
|
+
}
|
|
11154
|
+
return text.slice(0, end);
|
|
11155
|
+
}
|
|
11156
|
+
function trimSpaces(value) {
|
|
11157
|
+
let start = 0;
|
|
11158
|
+
let end = value.length;
|
|
11159
|
+
while (start < end && value.charCodeAt(start) === 32) start++;
|
|
11160
|
+
while (end > start && value.charCodeAt(end - 1) === 32) end--;
|
|
11161
|
+
return value.slice(start, end);
|
|
11146
11162
|
}
|
|
11147
11163
|
function analyzeDirectory(source) {
|
|
11148
11164
|
if (!isDicomdir(source.fileMeta)) return void 0;
|
|
@@ -13801,7 +13817,7 @@ function stripPad(bytes, tag, vr, position, warnings) {
|
|
|
13801
13817
|
}
|
|
13802
13818
|
return bytes.subarray(0, end);
|
|
13803
13819
|
}
|
|
13804
|
-
function
|
|
13820
|
+
function trimSpaces2(value) {
|
|
13805
13821
|
let start = 0;
|
|
13806
13822
|
let end = value.length;
|
|
13807
13823
|
while (start < end && value.charCodeAt(start) === SPACE2) start++;
|
|
@@ -13860,7 +13876,7 @@ function decodeElementValue(element) {
|
|
|
13860
13876
|
}
|
|
13861
13877
|
if (vr === "LO" || vr === "SH" || vr === "UC") {
|
|
13862
13878
|
const text = decodeCharsetString(trimmed, tag, vr, charset, position, warnings);
|
|
13863
|
-
const values = text.split("\\").map(
|
|
13879
|
+
const values = text.split("\\").map(trimSpaces2);
|
|
13864
13880
|
return attachWarnings({ kind: "strings", values }, warnings);
|
|
13865
13881
|
}
|
|
13866
13882
|
if (vr === "LT" || vr === "ST" || vr === "UT") {
|
|
@@ -13873,7 +13889,7 @@ function decodeElementValue(element) {
|
|
|
13873
13889
|
}
|
|
13874
13890
|
if (vr === "AE" || vr === "CS" || vr === "AS" || vr === "UI") {
|
|
13875
13891
|
const text = decodeAsciiString(trimmed, tag, vr, position, warnings);
|
|
13876
|
-
const values = text.split("\\").map(
|
|
13892
|
+
const values = text.split("\\").map(trimSpaces2);
|
|
13877
13893
|
return attachWarnings({ kind: "strings", values }, warnings);
|
|
13878
13894
|
}
|
|
13879
13895
|
if (vr === "DS") {
|
|
@@ -14942,8 +14958,43 @@ function toDate(value, options) {
|
|
|
14942
14958
|
const instant = new Date(utc.getTime() - offsetMinutes * 6e4);
|
|
14943
14959
|
return Number.isNaN(instant.getTime()) ? void 0 : instant;
|
|
14944
14960
|
}
|
|
14961
|
+
|
|
14962
|
+
// src/serialize/errors.ts
|
|
14963
|
+
var SERIALIZE_ERROR_CODES = {
|
|
14964
|
+
MISSING_TRANSFER_SYNTAX: "MISSING_TRANSFER_SYNTAX",
|
|
14965
|
+
UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX",
|
|
14966
|
+
INVALID_ENCAPSULATED_PIXEL_DATA: "INVALID_ENCAPSULATED_PIXEL_DATA",
|
|
14967
|
+
DIRECTORY_OFFSET_UNRESOLVED: "DIRECTORY_OFFSET_UNRESOLVED",
|
|
14968
|
+
DIRECTORY_OFFSET_DEFLATED: "DIRECTORY_OFFSET_DEFLATED",
|
|
14969
|
+
VALUE_TOO_LONG_FOR_VR: "VALUE_TOO_LONG_FOR_VR"
|
|
14970
|
+
};
|
|
14971
|
+
var DicomSerializeError = class extends Error {
|
|
14972
|
+
code;
|
|
14973
|
+
/**
|
|
14974
|
+
* Construct a new `DicomSerializeError`. The `message` MUST be built only
|
|
14975
|
+
* from structural facts (never a decoded attribute value) so the error is
|
|
14976
|
+
* always safe to log.
|
|
14977
|
+
*
|
|
14978
|
+
* @internal
|
|
14979
|
+
*/
|
|
14980
|
+
constructor(code, message) {
|
|
14981
|
+
super(`[${code}] ${message}`);
|
|
14982
|
+
this.name = "DicomSerializeError";
|
|
14983
|
+
this.code = code;
|
|
14984
|
+
}
|
|
14985
|
+
};
|
|
14986
|
+
|
|
14987
|
+
// src/serialize/element.ts
|
|
14945
14988
|
var UNDEFINED_LENGTH4 = 4294967295;
|
|
14946
14989
|
var MAX_SHORT_FORM_LENGTH = 65534;
|
|
14990
|
+
var PRIVATE_CREATOR_TOO_LONG_MESSAGE = "A Private Creator Data Element carries a value longer than 65,534 bytes under an Explicit VR Transfer Syntax, in a VR whose Value Length Field is 16 bits. PS3.5 section 6.2.2 has such a value written with VR UN, but says UN shall not be used for a Private Creator Data Element, so the value has no encoding and nothing is written.";
|
|
14991
|
+
var FILE_META_TOO_LONG_MESSAGE = "A File Meta Information element carries a value longer than 65,534 bytes in a VR whose Explicit VR Value Length Field is 16 bits. PS3.5 section 6.2.2 has such a value written with VR UN, but says UN shall not be used for a File Meta Information element, so the value has no encoding and nothing is written.";
|
|
14992
|
+
function refuseTooLongForVr(kind) {
|
|
14993
|
+
throw new DicomSerializeError(
|
|
14994
|
+
SERIALIZE_ERROR_CODES.VALUE_TOO_LONG_FOR_VR,
|
|
14995
|
+
kind === "privateCreator" ? PRIVATE_CREATOR_TOO_LONG_MESSAGE : FILE_META_TOO_LONG_MESSAGE
|
|
14996
|
+
);
|
|
14997
|
+
}
|
|
14947
14998
|
var NULL_PAD_VRS = /* @__PURE__ */ new Set(["UI", "OB", "OW", "OF", "OD", "OL", "OV", "UN"]);
|
|
14948
14999
|
function padValue(value, vr) {
|
|
14949
15000
|
if (value.length % 2 === 0) return buffer.Buffer.from(value);
|
|
@@ -15006,7 +15057,9 @@ function encodeDatasetElement(el, encoding, rawBytes = el.rawBytes) {
|
|
|
15006
15057
|
value
|
|
15007
15058
|
]);
|
|
15008
15059
|
}
|
|
15009
|
-
if (value.length > MAX_SHORT_FORM_LENGTH
|
|
15060
|
+
if (value.length > MAX_SHORT_FORM_LENGTH) {
|
|
15061
|
+
if (isPrivateCreator(group, element)) refuseTooLongForVr("privateCreator");
|
|
15062
|
+
if (group === 2) refuseTooLongForVr("fileMeta");
|
|
15010
15063
|
return buffer.Buffer.concat([
|
|
15011
15064
|
uint16(group, littleEndian),
|
|
15012
15065
|
uint16(element, littleEndian),
|
|
@@ -15025,30 +15078,6 @@ function encodeDatasetElement(el, encoding, rawBytes = el.rawBytes) {
|
|
|
15025
15078
|
value
|
|
15026
15079
|
]);
|
|
15027
15080
|
}
|
|
15028
|
-
|
|
15029
|
-
// src/serialize/errors.ts
|
|
15030
|
-
var SERIALIZE_ERROR_CODES = {
|
|
15031
|
-
MISSING_TRANSFER_SYNTAX: "MISSING_TRANSFER_SYNTAX",
|
|
15032
|
-
UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX",
|
|
15033
|
-
INVALID_ENCAPSULATED_PIXEL_DATA: "INVALID_ENCAPSULATED_PIXEL_DATA",
|
|
15034
|
-
DIRECTORY_OFFSET_UNRESOLVED: "DIRECTORY_OFFSET_UNRESOLVED",
|
|
15035
|
-
DIRECTORY_OFFSET_DEFLATED: "DIRECTORY_OFFSET_DEFLATED"
|
|
15036
|
-
};
|
|
15037
|
-
var DicomSerializeError = class extends Error {
|
|
15038
|
-
code;
|
|
15039
|
-
/**
|
|
15040
|
-
* Construct a new `DicomSerializeError`. The `message` MUST be built only
|
|
15041
|
-
* from structural facts (never a decoded attribute value) so the error is
|
|
15042
|
-
* always safe to log.
|
|
15043
|
-
*
|
|
15044
|
-
* @internal
|
|
15045
|
-
*/
|
|
15046
|
-
constructor(code, message) {
|
|
15047
|
-
super(`[${code}] ${message}`);
|
|
15048
|
-
this.name = "DicomSerializeError";
|
|
15049
|
-
this.code = code;
|
|
15050
|
-
}
|
|
15051
|
-
};
|
|
15052
15081
|
var UNDEFINED_LENGTH5 = 4294967295;
|
|
15053
15082
|
var ITEM2 = 4294893568;
|
|
15054
15083
|
var ITEM_DELIMITATION = 4294893581;
|
|
@@ -15493,6 +15522,7 @@ function encodeMetaElement(tag, vr, rawValue) {
|
|
|
15493
15522
|
value
|
|
15494
15523
|
]);
|
|
15495
15524
|
}
|
|
15525
|
+
if (value.length > MAX_SHORT_FORM_LENGTH) refuseTooLongForVr("fileMeta");
|
|
15496
15526
|
const lengthBuf = buffer.Buffer.alloc(2);
|
|
15497
15527
|
lengthBuf.writeUInt16LE(value.length, 0);
|
|
15498
15528
|
return buffer.Buffer.concat([groupBuf, elementBuf, vrBuf, lengthBuf, value]);
|
|
@@ -16766,14 +16796,19 @@ function dummyBytes(vr) {
|
|
|
16766
16796
|
const buf = buffer.Buffer.from(text, "latin1");
|
|
16767
16797
|
return buf.length % 2 === 0 ? buf : buffer.Buffer.concat([buf, buffer.Buffer.from([32])]);
|
|
16768
16798
|
}
|
|
16799
|
+
function stripTrailingNulls(text) {
|
|
16800
|
+
let end = text.length;
|
|
16801
|
+
while (end > 0 && text.charCodeAt(end - 1) === 0) end--;
|
|
16802
|
+
return text.slice(0, end);
|
|
16803
|
+
}
|
|
16769
16804
|
function remapUidBytes(rawBytes, remap) {
|
|
16770
|
-
const raw = rawBytes.toString("latin1")
|
|
16805
|
+
const raw = stripTrailingNulls(rawBytes.toString("latin1"));
|
|
16771
16806
|
const remapped = raw.split("\\").map((uid2) => remap(uid2.trim())).join("\\");
|
|
16772
16807
|
const buf = buffer.Buffer.from(remapped, "latin1");
|
|
16773
16808
|
return buf.length % 2 === 0 ? buf : buffer.Buffer.concat([buf, buffer.Buffer.from([0])]);
|
|
16774
16809
|
}
|
|
16775
16810
|
function uidValueMultiplicity(rawBytes) {
|
|
16776
|
-
const raw = rawBytes.toString("latin1")
|
|
16811
|
+
const raw = stripTrailingNulls(rawBytes.toString("latin1"));
|
|
16777
16812
|
if (raw.length === 0) return 0;
|
|
16778
16813
|
return raw.split("\\").length;
|
|
16779
16814
|
}
|
|
@@ -17175,7 +17210,7 @@ function insertedSequence(tag, items, encoding) {
|
|
|
17175
17210
|
});
|
|
17176
17211
|
}
|
|
17177
17212
|
function decodeCreator(el) {
|
|
17178
|
-
return el.rawBytes.toString("latin1").
|
|
17213
|
+
return trimTrailingPad(el.rawBytes).toString("latin1").trim();
|
|
17179
17214
|
}
|
|
17180
17215
|
function isPrivateCreatorElement(tag) {
|
|
17181
17216
|
const { element } = splitTag(tag);
|
|
@@ -17303,7 +17338,7 @@ function declarationKey(group, creator) {
|
|
|
17303
17338
|
return `${String(group)}:${creator}`;
|
|
17304
17339
|
}
|
|
17305
17340
|
function decodeEnumerated(el) {
|
|
17306
|
-
return el.rawBytes.toString("latin1").
|
|
17341
|
+
return trimTrailingPad(el.rawBytes).toString("latin1").trim();
|
|
17307
17342
|
}
|
|
17308
17343
|
function readUnsignedShorts(el) {
|
|
17309
17344
|
const values = [];
|
|
@@ -17740,7 +17775,7 @@ function hasUncleanedBurnedIn(ds) {
|
|
|
17740
17775
|
if (!ds.has(TAG_PIXEL_DATA)) return false;
|
|
17741
17776
|
const flag = ds.get(TAG_BURNED_IN_ANNOTATION);
|
|
17742
17777
|
if (flag === void 0) return true;
|
|
17743
|
-
return flag.rawBytes.toString("latin1").
|
|
17778
|
+
return trimTrailingPad(flag.rawBytes).toString("latin1").trim().toUpperCase() !== "NO";
|
|
17744
17779
|
}
|
|
17745
17780
|
function deidentify(ds, options = {}) {
|
|
17746
17781
|
if (isRefusedTransferSyntax(ds.fileMeta?.transferSyntaxUID)) {
|