@cosyte/dicom 0.0.1
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 +218 -0
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/dist/index.cjs +14492 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2577 -0
- package/dist/index.d.ts +2577 -0
- package/dist/index.mjs +14455 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +105 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,2577 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Package version string for `@cosyte/dicom`. Synchronized with `package.json#version` by
|
|
5
|
+
* `scripts/sync-version.mjs`, which the `version` script runs after `changeset version`
|
|
6
|
+
* so the bump and this constant land in the same "Version Packages" commit.
|
|
7
|
+
*
|
|
8
|
+
* Stays on the uniform `0.0.x`-until-first-alpha ladder (locked across the `@cosyte/*` suite):
|
|
9
|
+
* patch bumps via Changesets through pre-alpha, with no `0.1.0` milestone bump.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* import { VERSION } from "@cosyte/dicom";
|
|
13
|
+
* console.log(`@cosyte/dicom v${VERSION}`);
|
|
14
|
+
*/
|
|
15
|
+
declare const VERSION: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Type definitions for the Phase 1 public Dictionary namespace.
|
|
19
|
+
*
|
|
20
|
+
* Frozen surface. Phase 2 (parser), Phase 3 (dataset/VR), and Phase 7
|
|
21
|
+
* (validate) all depend on this
|
|
22
|
+
* shape — schema-breaking changes here cascade through the codebase.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* 8-character uppercase hex DICOM tag, e.g., `"00100010"` (Patient's Name).
|
|
28
|
+
*
|
|
29
|
+
* For repeating-group attribute families (`(50xx,xxxx)` curves, `(60xx,xxxx)`
|
|
30
|
+
* overlays, etc.), the tag string preserves lowercase `x` placeholders verbatim
|
|
31
|
+
* — those entries cannot be looked up by concrete tag and are flagged via
|
|
32
|
+
* {@link DictionaryEntry.repeatingGroup}.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const tag: Tag = "00100010"; // Patient's Name
|
|
36
|
+
*/
|
|
37
|
+
type Tag = string;
|
|
38
|
+
/**
|
|
39
|
+
* The 33 standard DICOM Value Representations from PS3.5 §6.2 plus the 64-bit
|
|
40
|
+
* additions (`OV`, `SV`, `UV`) introduced in DICOM 2018.
|
|
41
|
+
*
|
|
42
|
+
* Note that some attributes in the data dictionary list MULTIPLE possible VRs
|
|
43
|
+
* — see {@link DictionaryEntry.vr} (which is always an array, possibly empty
|
|
44
|
+
* for retired entries with no VR or special "See Note" entries).
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const vr: VR = "PN"; // Person Name
|
|
48
|
+
*/
|
|
49
|
+
type VR = "AE" | "AS" | "AT" | "CS" | "DA" | "DS" | "DT" | "FL" | "FD" | "IS" | "LO" | "LT" | "OB" | "OD" | "OF" | "OL" | "OV" | "OW" | "PN" | "SH" | "SL" | "SQ" | "SS" | "ST" | "SV" | "TM" | "UC" | "UI" | "UL" | "UN" | "UR" | "US" | "UT" | "UV";
|
|
50
|
+
/**
|
|
51
|
+
* One DICOM attribute as published in PS3.6 (Data Dictionary).
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* import { Dictionary } from "@cosyte/dicom";
|
|
55
|
+
* const entry = Dictionary.lookup("00100010");
|
|
56
|
+
* if (entry) {
|
|
57
|
+
* // entry.keyword === "PatientName"
|
|
58
|
+
* // entry.vr === ["PN"]
|
|
59
|
+
* }
|
|
60
|
+
*/
|
|
61
|
+
interface DictionaryEntry {
|
|
62
|
+
readonly tag: Tag;
|
|
63
|
+
readonly keyword: string;
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly vr: readonly VR[];
|
|
66
|
+
readonly vm: string;
|
|
67
|
+
readonly retired: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* `true` for repeating-group families (`(50xx,xxxx)`, `(60xx,xxxx)` —
|
|
70
|
+
* curves/overlays); the {@link DictionaryEntry.tag} field for these contains
|
|
71
|
+
* lowercase `x` placeholders, NOT a concrete 8-hex-char tag. Repeating-group
|
|
72
|
+
* entries are not surfaced by `Dictionary.lookup(tag)` for concrete tags.
|
|
73
|
+
*/
|
|
74
|
+
readonly repeatingGroup?: boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One DICOM UID as published in PS3.6 / PS3.4 (UID registry).
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* import { Dictionary } from "@cosyte/dicom";
|
|
81
|
+
* const ts = Dictionary.uid("1.2.840.10008.1.2.1");
|
|
82
|
+
* if (ts) {
|
|
83
|
+
* // ts.name === "Explicit VR Little Endian"
|
|
84
|
+
* // ts.type === "TransferSyntax"
|
|
85
|
+
* }
|
|
86
|
+
*/
|
|
87
|
+
interface UidEntry {
|
|
88
|
+
readonly uid: string;
|
|
89
|
+
readonly name: string;
|
|
90
|
+
readonly type: "TransferSyntax" | "SOPClass" | "MetaSOPClass" | "WellKnownFrameOfReference" | "WellKnownSOPInstance" | "CodingScheme" | "ApplicationContext" | "ServiceClass" | "Other";
|
|
91
|
+
readonly retired: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Public Dictionary namespace for `@cosyte/dicom` — Phase 1 deliverable.
|
|
96
|
+
*
|
|
97
|
+
* D-10:
|
|
98
|
+
*
|
|
99
|
+
* - {@link lookup} — accepts either an 8-char hex tag (`"00100010"`) or a
|
|
100
|
+
* keyword (`"PatientName"`) and returns the typed {@link DictionaryEntry}
|
|
101
|
+
* or `undefined`.
|
|
102
|
+
* - {@link byKeyword} — keyword-only lookup; for cases where the caller has
|
|
103
|
+
* already validated the input shape and wants a narrower call.
|
|
104
|
+
* - {@link uid} — UID lookup (DICT-06).
|
|
105
|
+
*
|
|
106
|
+
* No function throws on miss (D-10: "Returns the typed entry or `undefined`.
|
|
107
|
+
* No throws on miss."). Returned entries are deeply frozen — mutation attempts
|
|
108
|
+
* throw `TypeError` in strict mode (which all of `@cosyte/dicom`'s emitted code
|
|
109
|
+
* runs under, given `"use strict"` is implicit for ES modules).
|
|
110
|
+
*
|
|
111
|
+
* @module
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Look up a DICOM attribute by tag (`"00100010"`) or keyword (`"PatientName"`).
|
|
116
|
+
*
|
|
117
|
+
* Hex tag input is normalized to uppercase; keyword input is case-sensitive.
|
|
118
|
+
* Returns `undefined` for unknown tags, unknown keywords, malformed input, or
|
|
119
|
+
* tags from repeating-group families (those resolve via the family entry's
|
|
120
|
+
* `repeatingGroup` flag — see {@link DictionaryEntry}).
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* import { Dictionary } from "@cosyte/dicom";
|
|
124
|
+
* const a = Dictionary.lookup("00100010"); // by tag
|
|
125
|
+
* const b = Dictionary.lookup("PatientName"); // by keyword (DICT-04)
|
|
126
|
+
* const c = Dictionary.lookup("not-real"); // undefined (D-10 no-throw)
|
|
127
|
+
*/
|
|
128
|
+
declare function lookup(tagOrKeyword: string): DictionaryEntry | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Look up a DICOM attribute strictly by keyword (`"PatientName"`).
|
|
131
|
+
*
|
|
132
|
+
* Returns `undefined` for unknown keywords or malformed input. Use
|
|
133
|
+
* {@link lookup} if the input could be either a tag or a keyword.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* import { Dictionary } from "@cosyte/dicom";
|
|
137
|
+
* const e = Dictionary.byKeyword("StudyInstanceUID");
|
|
138
|
+
* // e?.tag === "0020000D"
|
|
139
|
+
*/
|
|
140
|
+
declare function byKeyword(keyword: string): DictionaryEntry | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* Look up a DICOM UID by its dotted-decimal value.
|
|
143
|
+
*
|
|
144
|
+
* Returns `undefined` for unknown UIDs or malformed input. Used by
|
|
145
|
+
* `parseDicom()` (Phase 2) to render human-readable Transfer Syntax names
|
|
146
|
+
* (DICT-06).
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* import { Dictionary } from "@cosyte/dicom";
|
|
150
|
+
* const ts = Dictionary.uid("1.2.840.10008.1.2.1");
|
|
151
|
+
* // ts?.name === "Explicit VR Little Endian"
|
|
152
|
+
* // ts?.type === "TransferSyntax"
|
|
153
|
+
*/
|
|
154
|
+
declare function uid(uidValue: string): UidEntry | undefined;
|
|
155
|
+
|
|
156
|
+
type index_DictionaryEntry = DictionaryEntry;
|
|
157
|
+
type index_Tag = Tag;
|
|
158
|
+
type index_UidEntry = UidEntry;
|
|
159
|
+
type index_VR = VR;
|
|
160
|
+
declare const index_byKeyword: typeof byKeyword;
|
|
161
|
+
declare const index_lookup: typeof lookup;
|
|
162
|
+
declare const index_uid: typeof uid;
|
|
163
|
+
declare namespace index {
|
|
164
|
+
export { type index_DictionaryEntry as DictionaryEntry, type index_Tag as Tag, type index_UidEntry as UidEntry, type index_VR as VR, index_byKeyword as byKeyword, index_lookup as lookup, index_uid as uid };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Shared parser-pipeline types for `@cosyte/dicom`.
|
|
169
|
+
*
|
|
170
|
+
* Phase 2 core-parser context:
|
|
171
|
+
* - D-02 — `ParseOptions` shape (Phase 2 only; no `profile` field).
|
|
172
|
+
* - D-03 — `OnWarningCallback` ordering contract (invoked AFTER push to `ctx.warnings`).
|
|
173
|
+
* - D-07 — `DicomPosition` shape (`byteOffset`, optional `fileMeta` / `deflated` / `contextPath`).
|
|
174
|
+
* - D-45 — `ParseContext.profile?: unknown` is reserved for Phase 6; Phase 2 never sets it.
|
|
175
|
+
*
|
|
176
|
+
* Public types (exported via `src/index.ts`): `DicomPosition`, `ParseOptions`, `OnWarningCallback`.
|
|
177
|
+
* Internal type (NOT exported from `src/index.ts`): `ParseContext`.
|
|
178
|
+
*
|
|
179
|
+
* @module
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* One private-data attribute definition supplied by a {@link Profile}'s
|
|
184
|
+
* private-dictionary overlay. The `vr` resolves the Implicit-VR of a private
|
|
185
|
+
* data element whose on-wire encoding carries no VR; `keyword` / `name` carry
|
|
186
|
+
* the vendor-documented identity for tooling and docs.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* import type { PrivateTagDefinition } from "@cosyte/dicom";
|
|
191
|
+
* const def: PrivateTagDefinition = {
|
|
192
|
+
* vr: "OB",
|
|
193
|
+
* keyword: "CSAImageHeaderInfo",
|
|
194
|
+
* name: "CSA Image Header Info",
|
|
195
|
+
* };
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
interface PrivateTagDefinition {
|
|
199
|
+
readonly vr: VR;
|
|
200
|
+
readonly keyword: string;
|
|
201
|
+
readonly name: string;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A source/vendor tolerance preset (Phase 6). A `Profile` bundles three
|
|
205
|
+
* things that only ever **tighten or annotate** a parse — never loosen it
|
|
206
|
+
* past the Postel's-Law default:
|
|
207
|
+
*
|
|
208
|
+
* - `escalations` — Tier-2 warning codes promoted to a thrown
|
|
209
|
+
* `DicomParseError` (a stricter posture for known-unsafe deviations).
|
|
210
|
+
* - `suppressions` — Tier-2 warning codes silenced because they are a
|
|
211
|
+
* documented, benign quirk of the named source (annotation, not loss).
|
|
212
|
+
* - `privateDictionary` — a private-creator-keyed overlay resolving the
|
|
213
|
+
* Implicit-VR of vendor private data elements via the file's **live**
|
|
214
|
+
* private-creator string (never a hard-coded block number).
|
|
215
|
+
*
|
|
216
|
+
* Build one with `defineProfile()`; never hand-author the frozen shape.
|
|
217
|
+
* Profiles are immutable and composable via `extends`.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* import { parseDicom, profiles } from "@cosyte/dicom";
|
|
222
|
+
* const ds = parseDicom(buf, { profile: profiles.siemens });
|
|
223
|
+
* console.log(ds.fileMeta?.transferSyntaxUID);
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
interface Profile {
|
|
227
|
+
readonly name: string;
|
|
228
|
+
readonly lineage: readonly string[];
|
|
229
|
+
readonly description?: string;
|
|
230
|
+
readonly escalations: ReadonlySet<WarningCode>;
|
|
231
|
+
readonly suppressions: ReadonlySet<WarningCode>;
|
|
232
|
+
/**
|
|
233
|
+
* Creator string → canonical private-tag key (`"GGGGxxEE"`, e.g.
|
|
234
|
+
* `"0029xx10"`) → definition. The `xx` placeholder stands for the
|
|
235
|
+
* file-assigned private block byte, mirroring the published DICOM
|
|
236
|
+
* private-dictionary notation; resolution is therefore by creator string,
|
|
237
|
+
* never by a fixed block number.
|
|
238
|
+
*/
|
|
239
|
+
readonly privateDictionary: ReadonlyMap<string, ReadonlyMap<string, PrivateTagDefinition>>;
|
|
240
|
+
/** Render a human-readable, deterministic one-line summary of the profile. */
|
|
241
|
+
readonly describe?: () => string;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Positional context for a `DicomParseWarning` or `DicomParseError`.
|
|
245
|
+
*
|
|
246
|
+
* Byte offsets are relative to the source buffer for non-deflated transfer
|
|
247
|
+
* syntaxes; for the Deflated Explicit VR LE transfer syntax (D-27),
|
|
248
|
+
* `deflated: true` indicates the offset is into the inflated dataset buffer
|
|
249
|
+
* rather than the on-disk source.
|
|
250
|
+
*
|
|
251
|
+
* With `exactOptionalPropertyTypes: true`, callers should omit unset keys
|
|
252
|
+
* rather than passing `undefined` (mirrors `@cosyte/hl7` sibling discipline).
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```ts
|
|
256
|
+
* import type { DicomPosition } from "@cosyte/dicom";
|
|
257
|
+
* const p: DicomPosition = { byteOffset: 132, fileMeta: true };
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
interface DicomPosition {
|
|
261
|
+
readonly byteOffset: number;
|
|
262
|
+
/** True when offset is inside the File Meta group. Omit (do not pass `undefined`) when not applicable. */
|
|
263
|
+
readonly fileMeta?: boolean;
|
|
264
|
+
/** True when offset is into the inflated dataset buffer (Deflated TS only). Omit when not applicable. */
|
|
265
|
+
readonly deflated?: boolean;
|
|
266
|
+
/** Tag chain for nested SQ items, e.g. `["0040A730", "0", "00080100"]`. Omit when at root. */
|
|
267
|
+
readonly contextPath?: readonly string[];
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Synchronous callback invoked once per Tier-2 warning emitted during parse.
|
|
271
|
+
*
|
|
272
|
+
* Per `02-CONTEXT.md` D-03, the callback fires AFTER the warning has been
|
|
273
|
+
* pushed to `ctx.warnings`; if the callback throws, the parser silently
|
|
274
|
+
* swallows the exception and continues (mirrors `@cosyte/hl7` sibling).
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* import type { OnWarningCallback } from "@cosyte/dicom";
|
|
279
|
+
* const onWarning: OnWarningCallback = (w) => {
|
|
280
|
+
* if (w.code === "DICOM_MISSING_PREAMBLE") {
|
|
281
|
+
* // ...
|
|
282
|
+
* }
|
|
283
|
+
* };
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
type OnWarningCallback = (warning: DicomParseWarning) => void;
|
|
287
|
+
/**
|
|
288
|
+
* Options accepted by `parseDicom`.
|
|
289
|
+
*
|
|
290
|
+
* Per `02-CONTEXT.md` D-02 — Phase 2 form only. No `profile` field; Phase 6
|
|
291
|
+
* adds it. With `exactOptionalPropertyTypes: true`, callers omit unset keys
|
|
292
|
+
* rather than passing `undefined` for any field below.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```ts
|
|
296
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
297
|
+
* const ds = parseDicom(buf, {
|
|
298
|
+
* strict: false,
|
|
299
|
+
* stripPreamble: "tolerate",
|
|
300
|
+
* copyValues: false,
|
|
301
|
+
* onWarning: (w) => console.warn(w.code),
|
|
302
|
+
* });
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
interface ParseOptions {
|
|
306
|
+
/**
|
|
307
|
+
* When `true`, every Tier-2 warning is escalated to a thrown
|
|
308
|
+
* `DicomParseError` carrying the warning code. Default `false`.
|
|
309
|
+
*
|
|
310
|
+
* Omit (do not pass `undefined`) to use the default.
|
|
311
|
+
*/
|
|
312
|
+
readonly strict?: boolean;
|
|
313
|
+
/**
|
|
314
|
+
* Preamble policy:
|
|
315
|
+
* - `"tolerate"` (default): attempt to start at offset 0 if `DICM` magic
|
|
316
|
+
* is missing at offset 128; emit `DICOM_MISSING_PREAMBLE`.
|
|
317
|
+
* - `"require"`: throw `DicomParseError(NOT_DICOM_PART_10)` when no
|
|
318
|
+
* `DICM` magic is present.
|
|
319
|
+
*
|
|
320
|
+
* Omit to use the default.
|
|
321
|
+
*/
|
|
322
|
+
readonly stripPreamble?: "tolerate" | "require";
|
|
323
|
+
/**
|
|
324
|
+
* Synchronous callback invoked once per Tier-2 warning, after the warning
|
|
325
|
+
* has been pushed to `Dataset.warnings`. Throwing handlers are silently
|
|
326
|
+
* swallowed (parser-state safety per D-03).
|
|
327
|
+
*
|
|
328
|
+
* Omit to skip the callback entirely.
|
|
329
|
+
*/
|
|
330
|
+
readonly onWarning?: OnWarningCallback;
|
|
331
|
+
/**
|
|
332
|
+
* When `true`, every `Element.rawBytes` is `Buffer.from(slice)` — copying
|
|
333
|
+
* each value out so the source buffer can be released. When `false` (the
|
|
334
|
+
* default), `Element.rawBytes` is `Buffer.subarray(slice)` — a zero-copy
|
|
335
|
+
* view that pins the source ArrayBuffer until every Element is GC'd.
|
|
336
|
+
*
|
|
337
|
+
* Per D-16 / MODEL-03. Omit to use the default.
|
|
338
|
+
*/
|
|
339
|
+
readonly copyValues?: boolean;
|
|
340
|
+
/**
|
|
341
|
+
* Source/vendor tolerance preset (Phase 6, D-45). Applies the profile's
|
|
342
|
+
* `escalations` / `suppressions` to warning emission and its
|
|
343
|
+
* `privateDictionary` to Implicit-VR resolution of private data elements.
|
|
344
|
+
* A profile only tightens or annotates — it never makes the default
|
|
345
|
+
* lenient parse throw outside the four Tier-3 fatals, and a private
|
|
346
|
+
* creator the profile does not recognize degrades to generic UN handling
|
|
347
|
+
* plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning, never a wrong decode.
|
|
348
|
+
*
|
|
349
|
+
* Omit (do not pass `undefined`) for the unprofiled default behaviour.
|
|
350
|
+
*/
|
|
351
|
+
readonly profile?: Profile;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Tier-2 warning registry and factories for the `@cosyte/dicom` parser
|
|
356
|
+
* pipeline.
|
|
357
|
+
*
|
|
358
|
+
* Phase 2 core-parser context:
|
|
359
|
+
* - D-08 — `WARNING_CODES` is a frozen `as const` registry with every code
|
|
360
|
+
* in the TOL-03 catalog (≥25 entries). Phase 2 actively emits 13;
|
|
361
|
+
* 7 VR-decode-time codes are reserved for Phase 3; 2 charset codes are
|
|
362
|
+
* reserved for Phase 4; 2 codes are reserved for Phase 6 / Phase 7.
|
|
363
|
+
* - D-12 — Exactly one named factory per actively-emitted code; each
|
|
364
|
+
* factory carries its own JSDoc + `@example` and returns a typed
|
|
365
|
+
* `DicomParseWarning`.
|
|
366
|
+
*
|
|
367
|
+
* Consumers compare `warning.code === WARNING_CODES.<CODE>` to narrow and
|
|
368
|
+
* react; the parser uses the factories here so message templates, payload
|
|
369
|
+
* shape, and positional context stay consistent across stages.
|
|
370
|
+
*
|
|
371
|
+
* @module
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Stable string codes for every Tier-2 warning the parser may emit.
|
|
376
|
+
*
|
|
377
|
+
* The registry is frozen via `as const` so TypeScript infers the exact
|
|
378
|
+
* string-literal union for `WarningCode` — there is zero runtime cost and
|
|
379
|
+
* no magic-string comparisons for consumers. Reserved-but-not-emitted
|
|
380
|
+
* codes carry inline comments documenting which phase activates them
|
|
381
|
+
* (Phase 2 declares the union so the schema is stable for downstream
|
|
382
|
+
* phases per D-08, D-42, D-43).
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* import { parseDicom, WARNING_CODES } from "@cosyte/dicom";
|
|
387
|
+
* const ds = parseDicom(buf);
|
|
388
|
+
* if (ds.warnings.some((w) => w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE)) {
|
|
389
|
+
* // handle bare File Meta input
|
|
390
|
+
* }
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
declare const WARNING_CODES: {
|
|
394
|
+
readonly DICOM_EMPTY_ITEM_IN_SEQUENCE: "DICOM_EMPTY_ITEM_IN_SEQUENCE";
|
|
395
|
+
readonly DICOM_FILE_META_GROUP_LENGTH_MISMATCH: "DICOM_FILE_META_GROUP_LENGTH_MISMATCH";
|
|
396
|
+
readonly DICOM_FILE_META_GROUP_LENGTH_MISSING: "DICOM_FILE_META_GROUP_LENGTH_MISSING";
|
|
397
|
+
readonly DICOM_GROUP_LENGTH_IN_DATASET: "DICOM_GROUP_LENGTH_IN_DATASET";
|
|
398
|
+
readonly DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR";
|
|
399
|
+
readonly DICOM_MISSING_PREAMBLE: "DICOM_MISSING_PREAMBLE";
|
|
400
|
+
readonly DICOM_NONZERO_RESERVED_BYTES: "DICOM_NONZERO_RESERVED_BYTES";
|
|
401
|
+
readonly DICOM_ODD_LENGTH_VALUE_PADDED: "DICOM_ODD_LENGTH_VALUE_PADDED";
|
|
402
|
+
readonly DICOM_PIXEL_DATA_LENGTH_MISMATCH: "DICOM_PIXEL_DATA_LENGTH_MISMATCH";
|
|
403
|
+
readonly DICOM_PRIVATE_TAG_NO_CREATOR: "DICOM_PRIVATE_TAG_NO_CREATOR";
|
|
404
|
+
readonly DICOM_UN_PARSED_AS_SQ: "DICOM_UN_PARSED_AS_SQ";
|
|
405
|
+
readonly DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR: "DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR";
|
|
406
|
+
readonly DICOM_VR_MISMATCH: "DICOM_VR_MISMATCH";
|
|
407
|
+
readonly DICOM_BOM_IN_TEXT_VR: "DICOM_BOM_IN_TEXT_VR";
|
|
408
|
+
readonly DICOM_DA_LEGACY_FORMAT: "DICOM_DA_LEGACY_FORMAT";
|
|
409
|
+
readonly DICOM_DT_NONSTANDARD_OFFSET: "DICOM_DT_NONSTANDARD_OFFSET";
|
|
410
|
+
readonly DICOM_IS_NONINTEGER_VALUE: "DICOM_IS_NONINTEGER_VALUE";
|
|
411
|
+
readonly DICOM_NON_ASCII_IN_ASCII_VR: "DICOM_NON_ASCII_IN_ASCII_VR";
|
|
412
|
+
readonly DICOM_TRAILING_NULL_IN_TEXT_VR: "DICOM_TRAILING_NULL_IN_TEXT_VR";
|
|
413
|
+
readonly DICOM_UI_TRAILING_SPACE: "DICOM_UI_TRAILING_SPACE";
|
|
414
|
+
readonly DICOM_CHARSET_AMBIGUOUS_SEPARATOR: "DICOM_CHARSET_AMBIGUOUS_SEPARATOR";
|
|
415
|
+
readonly DICOM_UNSUPPORTED_CHARSET: "DICOM_UNSUPPORTED_CHARSET";
|
|
416
|
+
readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
|
|
417
|
+
readonly DICOM_PRIVATE_CREATOR_UNKNOWN: "DICOM_PRIVATE_CREATOR_UNKNOWN";
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Discriminant type for `DicomParseWarning.code`. Narrowing a warning by
|
|
421
|
+
* this code lets consumers write exhaustive `switch` blocks (enabled by
|
|
422
|
+
* the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free
|
|
423
|
+
* comparison against the `WARNING_CODES` registry.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```ts
|
|
427
|
+
* import type { DicomParseWarning, WarningCode } from "@cosyte/dicom";
|
|
428
|
+
* function describe(w: DicomParseWarning): string {
|
|
429
|
+
* const code: WarningCode = w.code;
|
|
430
|
+
* if (code === "DICOM_MISSING_PREAMBLE") return "preamble missing";
|
|
431
|
+
* return `warning: ${code}`;
|
|
432
|
+
* }
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
type WarningCode = (typeof WARNING_CODES)[keyof typeof WARNING_CODES];
|
|
436
|
+
/**
|
|
437
|
+
* Data shape for every Tier-2 warning emitted by the parser. Warnings are
|
|
438
|
+
* plain data (distinct from `DicomParseError`, which is a thrown `Error`
|
|
439
|
+
* subclass) so they can be safely accumulated into `Dataset.warnings` and
|
|
440
|
+
* passed to `onWarning` callbacks.
|
|
441
|
+
*
|
|
442
|
+
* Per D-07 there is intentionally NO `snippet` field on warnings —
|
|
443
|
+
* real-world files routinely produce 50+ warnings and a per-warning
|
|
444
|
+
* snippet would balloon retained memory. Snippets appear only on
|
|
445
|
+
* `DicomParseError` (the strict-mode escalation path).
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* ```ts
|
|
449
|
+
* import type { DicomParseWarning } from "@cosyte/dicom";
|
|
450
|
+
* const w: DicomParseWarning = {
|
|
451
|
+
* code: "DICOM_MISSING_PREAMBLE",
|
|
452
|
+
* message: "No DICM magic at offset 128.",
|
|
453
|
+
* position: { byteOffset: 0 },
|
|
454
|
+
* };
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
interface DicomParseWarning {
|
|
458
|
+
readonly code: WarningCode;
|
|
459
|
+
readonly message: string;
|
|
460
|
+
readonly position: DicomPosition;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Phase 2 structural `Item` — a single `(FFFE,E000)`-delimited Item
|
|
465
|
+
* inside a Sequence, carrying a nested `Dataset`.
|
|
466
|
+
*
|
|
467
|
+
* Per `02-CONTEXT.md` D-04: structural surface only; navigation methods
|
|
468
|
+
* land in Phase 3 (via `Dataset` extension per D-42).
|
|
469
|
+
*
|
|
470
|
+
* @module
|
|
471
|
+
*/
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Initialiser shape for an `Item` — extends `DatasetInit` with `index`,
|
|
475
|
+
* the 0-based position of the item inside its parent `Sequence`.
|
|
476
|
+
*
|
|
477
|
+
* @internal
|
|
478
|
+
*/
|
|
479
|
+
interface ItemInit extends DatasetInit {
|
|
480
|
+
readonly index: number;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* One sequence item. Inherits `fileMeta` (always `undefined` for nested
|
|
484
|
+
* items), `warnings`, and the protected element map from `Dataset`.
|
|
485
|
+
*
|
|
486
|
+
* Phase 3 surfaces `Item.get(...)` / `Item.has(...)` / etc. via the
|
|
487
|
+
* `Dataset` superclass extension per D-42.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```ts
|
|
491
|
+
* import { Item } from "@cosyte/dicom";
|
|
492
|
+
* // Producers (parser plan 02-04) construct items as follows:
|
|
493
|
+
* // const item = new Item({ index: 0, warnings: [], elements: new Map() });
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
declare class Item extends Dataset {
|
|
497
|
+
readonly index: number;
|
|
498
|
+
/**
|
|
499
|
+
* Construct a new structural `Item`. Producers are the SQ / FFFE
|
|
500
|
+
* marker parsers in plan 02-04.
|
|
501
|
+
*
|
|
502
|
+
* @internal
|
|
503
|
+
*/
|
|
504
|
+
constructor(init: ItemInit);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Decoded-value model for Phase 3 — the typed result of `Element.value`.
|
|
509
|
+
*
|
|
510
|
+
* `DicomValue` is a discriminated union keyed on `kind`; each variant maps
|
|
511
|
+
* to a family of VRs that share a decode shape (per PS3.5 §6.2 Table 6.2-1):
|
|
512
|
+
*
|
|
513
|
+
* - `empty` — zero-length value (any VR).
|
|
514
|
+
* - `text` — single-value text VRs `LT ST UT UR` (no backslash
|
|
515
|
+
* multiplicity; charset-decoded except `UR`).
|
|
516
|
+
* - `strings` — backslash-multi-valued string VRs
|
|
517
|
+
* `AE AS CS LO SH UC UI` (charset-decoded for
|
|
518
|
+
* `LO SH UC`; ASCII for the rest).
|
|
519
|
+
* - `personName` — `PN`, decoded to its 3-group / 5-component structure.
|
|
520
|
+
* - `numbers` — binary numeric VRs `US UL SS SL FL FD` (signedness
|
|
521
|
+
* from the VR, never guessed).
|
|
522
|
+
* - `bigints` — 64-bit binary VRs `SV UV` (kept as `bigint` so no
|
|
523
|
+
* precision is lost above 2^53).
|
|
524
|
+
* - `attributeTags` — `AT`, decoded to 8-hex tag strings.
|
|
525
|
+
* - `decimalString` — `DS`, parsed to `number | null` (non-numeric → null).
|
|
526
|
+
* - `integerString` — `IS`, parsed to `number | null` (non-integer → null).
|
|
527
|
+
* - `dates` — `DA`, tolerant temporal decode.
|
|
528
|
+
* - `times` — `TM`, tolerant temporal decode.
|
|
529
|
+
* - `dateTimes` — `DT`, tolerant temporal decode.
|
|
530
|
+
* - `binary` — bulk byte VRs `OB OD OF OL OV OW UN` (raw preserved;
|
|
531
|
+
* not interpreted in v1 — avoids the giant-typed-array
|
|
532
|
+
* footgun for pixel / LUT payloads).
|
|
533
|
+
* - `sequence` — `SQ`, the parsed `Item[]`.
|
|
534
|
+
*
|
|
535
|
+
* Every variant whose decode may flag a tolerated deviation carries an
|
|
536
|
+
* optional `warnings` array (decode is lazy and post-parse, so these cannot
|
|
537
|
+
* be folded into the frozen `Dataset.warnings`; they ride on the value).
|
|
538
|
+
*
|
|
539
|
+
* @module
|
|
540
|
+
*/
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* One of the three component groups of a `PN` value (PS3.5 §6.2.1.1):
|
|
544
|
+
* alphabetic, ideographic, or phonetic. Each holds the five `^`-delimited
|
|
545
|
+
* components; missing components are the empty string (never `undefined`).
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```ts
|
|
549
|
+
* import type { PersonNameGroup } from "@cosyte/dicom";
|
|
550
|
+
* const g: PersonNameGroup = {
|
|
551
|
+
* familyName: "Doe",
|
|
552
|
+
* givenName: "Jane",
|
|
553
|
+
* middleName: "",
|
|
554
|
+
* namePrefix: "",
|
|
555
|
+
* nameSuffix: "",
|
|
556
|
+
* };
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
interface PersonNameGroup {
|
|
560
|
+
readonly familyName: string;
|
|
561
|
+
readonly givenName: string;
|
|
562
|
+
readonly middleName: string;
|
|
563
|
+
readonly namePrefix: string;
|
|
564
|
+
readonly nameSuffix: string;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* A decoded `PN` value — up to three component groups separated on-wire by
|
|
568
|
+
* `=` (PS3.5 §6.2.1.1). `alphabetic` is always present; `ideographic` and
|
|
569
|
+
* `phonetic` are present only when the value supplied them.
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* ```ts
|
|
573
|
+
* import type { PersonName } from "@cosyte/dicom";
|
|
574
|
+
* // "Yamada^Tarou=山田^太郎=やまだ^たろう"
|
|
575
|
+
* declare const pn: PersonName;
|
|
576
|
+
* pn.alphabetic.familyName; // "Yamada"
|
|
577
|
+
* pn.ideographic?.familyName; // "山田"
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
interface PersonName {
|
|
581
|
+
readonly alphabetic: PersonNameGroup;
|
|
582
|
+
readonly ideographic?: PersonNameGroup;
|
|
583
|
+
readonly phonetic?: PersonNameGroup;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* A tolerantly-decoded `DA` (Date) value. `raw` always carries the on-wire
|
|
587
|
+
* string (PHI-safe — a date is not an identifier on its own, but is
|
|
588
|
+
* preserved verbatim regardless). `valid` is `true` only when the string
|
|
589
|
+
* parsed cleanly to a `YYYYMMDD` calendar date; otherwise the numeric
|
|
590
|
+
* fields are omitted and `raw` is the source of truth.
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```ts
|
|
594
|
+
* import type { DicomDate } from "@cosyte/dicom";
|
|
595
|
+
* const d: DicomDate = { raw: "20240115", valid: true, year: 2024, month: 1, day: 15 };
|
|
596
|
+
* ```
|
|
597
|
+
*/
|
|
598
|
+
interface DicomDate {
|
|
599
|
+
readonly raw: string;
|
|
600
|
+
readonly valid: boolean;
|
|
601
|
+
readonly year?: number;
|
|
602
|
+
readonly month?: number;
|
|
603
|
+
readonly day?: number;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* A tolerantly-decoded `TM` (Time) value (PS3.5 §6.2; max length 14 bytes).
|
|
607
|
+
* `fractionalSeconds` is the value after the decimal point as a number in
|
|
608
|
+
* `[0,1)` when present.
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```ts
|
|
612
|
+
* import type { DicomTime } from "@cosyte/dicom";
|
|
613
|
+
* const t: DicomTime = { raw: "133015.250000", valid: true, hours: 13, minutes: 30, seconds: 15, fractionalSeconds: 0.25 };
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
interface DicomTime {
|
|
617
|
+
readonly raw: string;
|
|
618
|
+
readonly valid: boolean;
|
|
619
|
+
readonly hours?: number;
|
|
620
|
+
readonly minutes?: number;
|
|
621
|
+
readonly seconds?: number;
|
|
622
|
+
readonly fractionalSeconds?: number;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* A tolerantly-decoded `DT` (DateTime) value (PS3.5 §6.2). `offsetMinutes`
|
|
626
|
+
* is the signed UTC offset (`&ZZXX` suffix) in minutes when present.
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* ```ts
|
|
630
|
+
* import type { DicomDateTime } from "@cosyte/dicom";
|
|
631
|
+
* const dt: DicomDateTime = { raw: "20240115133015", valid: true, year: 2024, month: 1, day: 15, hours: 13, minutes: 30, seconds: 15 };
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
interface DicomDateTime {
|
|
635
|
+
readonly raw: string;
|
|
636
|
+
readonly valid: boolean;
|
|
637
|
+
readonly year?: number;
|
|
638
|
+
readonly month?: number;
|
|
639
|
+
readonly day?: number;
|
|
640
|
+
readonly hours?: number;
|
|
641
|
+
readonly minutes?: number;
|
|
642
|
+
readonly seconds?: number;
|
|
643
|
+
readonly fractionalSeconds?: number;
|
|
644
|
+
readonly offsetMinutes?: number;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* The lazily-decoded value of an {@link Element}, as a discriminated union
|
|
648
|
+
* on `kind`. See the module doc for the VR → kind mapping. Narrow on `kind`
|
|
649
|
+
* (the `switch-exhaustiveness-check` lint rule keeps consumers honest).
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* ```ts
|
|
653
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
654
|
+
* const ds = parseDicom(buf);
|
|
655
|
+
* const v = ds.get("00100010")?.value; // Patient's Name (PN)
|
|
656
|
+
* if (v?.kind === "personName") {
|
|
657
|
+
* console.log(v.values[0]?.alphabetic.familyName);
|
|
658
|
+
* }
|
|
659
|
+
* ```
|
|
660
|
+
*/
|
|
661
|
+
type DicomValue = {
|
|
662
|
+
readonly kind: "empty";
|
|
663
|
+
} | {
|
|
664
|
+
readonly kind: "text";
|
|
665
|
+
readonly value: string;
|
|
666
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
667
|
+
} | {
|
|
668
|
+
readonly kind: "strings";
|
|
669
|
+
readonly values: readonly string[];
|
|
670
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
671
|
+
} | {
|
|
672
|
+
readonly kind: "personName";
|
|
673
|
+
readonly values: readonly PersonName[];
|
|
674
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
675
|
+
} | {
|
|
676
|
+
readonly kind: "numbers";
|
|
677
|
+
readonly values: readonly number[];
|
|
678
|
+
} | {
|
|
679
|
+
readonly kind: "bigints";
|
|
680
|
+
readonly values: readonly bigint[];
|
|
681
|
+
} | {
|
|
682
|
+
readonly kind: "attributeTags";
|
|
683
|
+
readonly values: readonly Tag[];
|
|
684
|
+
} | {
|
|
685
|
+
readonly kind: "decimalString";
|
|
686
|
+
readonly values: readonly (number | null)[];
|
|
687
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
688
|
+
} | {
|
|
689
|
+
readonly kind: "integerString";
|
|
690
|
+
readonly values: readonly (number | null)[];
|
|
691
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
692
|
+
} | {
|
|
693
|
+
readonly kind: "dates";
|
|
694
|
+
readonly values: readonly DicomDate[];
|
|
695
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
696
|
+
} | {
|
|
697
|
+
readonly kind: "times";
|
|
698
|
+
readonly values: readonly DicomTime[];
|
|
699
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
700
|
+
} | {
|
|
701
|
+
readonly kind: "dateTimes";
|
|
702
|
+
readonly values: readonly DicomDateTime[];
|
|
703
|
+
readonly warnings?: readonly DicomParseWarning[];
|
|
704
|
+
} | {
|
|
705
|
+
readonly kind: "binary";
|
|
706
|
+
readonly bytes: Buffer;
|
|
707
|
+
} | {
|
|
708
|
+
readonly kind: "sequence";
|
|
709
|
+
readonly items: readonly Item[];
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Phase 2 structural `Element` — leaf wrapper over an on-wire DICOM
|
|
714
|
+
* Data Element.
|
|
715
|
+
*
|
|
716
|
+
* Per `02-CONTEXT.md` D-04 + D-42: structural fields ONLY. No `.value`
|
|
717
|
+
* getter, no decoders, no `.items` navigation — those land in Phase 3,
|
|
718
|
+
* which extends this class.
|
|
719
|
+
*
|
|
720
|
+
* Per D-16: `rawBytes` is a `Buffer.subarray()` view over the source
|
|
721
|
+
* buffer by default (zero-copy), pinning the source ArrayBuffer until
|
|
722
|
+
* every Element is GC'd. The `parseDicom` `{ copyValues: true }` option
|
|
723
|
+
* flips production to `Buffer.from(slice)` so the source can be
|
|
724
|
+
* released; producers (parser plans 02-04 onward) honour it.
|
|
725
|
+
*
|
|
726
|
+
* @module
|
|
727
|
+
*/
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Initialiser shape accepted by the `Element` constructor.
|
|
731
|
+
*
|
|
732
|
+
* `privateCreator` is omitted (not `undefined`) for standard tags;
|
|
733
|
+
* present only when the parser resolved a `(gggg,EEFF)` private element
|
|
734
|
+
* to its registered Private Creator string per D-33 / D-34.
|
|
735
|
+
*
|
|
736
|
+
* @internal
|
|
737
|
+
*/
|
|
738
|
+
interface ElementInit {
|
|
739
|
+
readonly tag: Tag;
|
|
740
|
+
readonly vr: VR;
|
|
741
|
+
readonly vm: number;
|
|
742
|
+
readonly length: number;
|
|
743
|
+
readonly rawBytes: Buffer;
|
|
744
|
+
readonly byteOffset: number;
|
|
745
|
+
readonly privateCreator?: string;
|
|
746
|
+
/**
|
|
747
|
+
* Phase 2 hint set by the parser when an `Element` was promoted from
|
|
748
|
+
* `VR=UN` with undefined length to `VR=SQ` via the CP-246 fallback (D-30).
|
|
749
|
+
* Phase 3 reads this to choose the inner SQ decoder (Implicit VR LE) on
|
|
750
|
+
* first lazy access. Never set on standard SQ elements.
|
|
751
|
+
*
|
|
752
|
+
* @internal
|
|
753
|
+
*/
|
|
754
|
+
readonly cp246Promoted?: boolean;
|
|
755
|
+
/**
|
|
756
|
+
* Byte order of the value bytes, set by the parser per transfer syntax
|
|
757
|
+
* (`true` for Implicit/Explicit VR LE + Deflated; `false` for Explicit VR
|
|
758
|
+
* BE). Phase 3 numeric decoders read this — signedness comes from the VR,
|
|
759
|
+
* endianness from here.
|
|
760
|
+
*/
|
|
761
|
+
readonly littleEndian: boolean;
|
|
762
|
+
/**
|
|
763
|
+
* The dataset's resolved `(0008,0005)` Specific Character Set terms in
|
|
764
|
+
* effect for this element (parent dataset's, or the item's own override).
|
|
765
|
+
* Omitted when the Default Repertoire (ISO_IR 6) applies. Phase 3
|
|
766
|
+
* charset-dependent text decoders (`LO SH UC LT ST UT PN`) read this.
|
|
767
|
+
*/
|
|
768
|
+
readonly specificCharacterSet?: readonly string[];
|
|
769
|
+
/**
|
|
770
|
+
* For `SQ` elements: the parsed sequence items, threaded on by the parser
|
|
771
|
+
* so `Element.value` can expose them without re-parsing. Omitted for
|
|
772
|
+
* non-SQ elements.
|
|
773
|
+
*/
|
|
774
|
+
readonly items?: readonly Item[];
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* One DICOM Data Element as parsed by Phase 2.
|
|
778
|
+
*
|
|
779
|
+
* Phase 2 surface (this plan): `tag`, `vr`, `vm`, `length`, `rawBytes`,
|
|
780
|
+
* `byteOffset`, `privateCreator`. NO `.value` getter, NO decoders, NO
|
|
781
|
+
* navigation methods. Phase 3 (per D-42) extends this class with a lazy,
|
|
782
|
+
* memoized `.value` getter and the VR-aware decoders under
|
|
783
|
+
* `src/dataset/vr/`.
|
|
784
|
+
*
|
|
785
|
+
* `rawBytes` retention behaviour: zero-copy `Buffer.subarray` view by
|
|
786
|
+
* default (pins source ArrayBuffer); pass `{ copyValues: true }` to
|
|
787
|
+
* `parseDicom` for `Buffer.from(slice)` per element when the source
|
|
788
|
+
* needs to be released.
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* ```ts
|
|
792
|
+
* import { Buffer } from "node:buffer";
|
|
793
|
+
* import { Element } from "@cosyte/dicom";
|
|
794
|
+
* const el = new Element({
|
|
795
|
+
* tag: "00100010",
|
|
796
|
+
* vr: "PN",
|
|
797
|
+
* vm: 1,
|
|
798
|
+
* length: 8,
|
|
799
|
+
* rawBytes: Buffer.from("DOE^JANE"),
|
|
800
|
+
* byteOffset: 200,
|
|
801
|
+
* littleEndian: true,
|
|
802
|
+
* });
|
|
803
|
+
* // el.tag === "00100010"; el.rawBytes is a Buffer of 8 bytes.
|
|
804
|
+
* // el.value.kind === "personName".
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
declare class Element {
|
|
808
|
+
readonly tag: Tag;
|
|
809
|
+
readonly vr: VR;
|
|
810
|
+
readonly vm: number;
|
|
811
|
+
readonly length: number;
|
|
812
|
+
readonly rawBytes: Buffer;
|
|
813
|
+
readonly byteOffset: number;
|
|
814
|
+
readonly privateCreator: string | undefined;
|
|
815
|
+
/**
|
|
816
|
+
* Phase 2 hint for Phase 3's lazy SQ decoder per D-30. `true` when this
|
|
817
|
+
* Element was promoted from `VR=UN` with undefined length to `VR=SQ` via
|
|
818
|
+
* the CP-246 fallback. Phase 3 uses this to choose the Implicit VR LE
|
|
819
|
+
* inner decoder. Always `undefined` on standard SQ elements.
|
|
820
|
+
*
|
|
821
|
+
* @internal
|
|
822
|
+
*/
|
|
823
|
+
readonly cp246Promoted: boolean | undefined;
|
|
824
|
+
/** Value byte order (per transfer syntax). See {@link ElementInit.littleEndian}. */
|
|
825
|
+
readonly littleEndian: boolean;
|
|
826
|
+
/** In-effect `(0008,0005)` terms, or `undefined` for the Default Repertoire. */
|
|
827
|
+
readonly specificCharacterSet: readonly string[] | undefined;
|
|
828
|
+
/** Parsed items for an `SQ` element; `undefined` otherwise. */
|
|
829
|
+
readonly items: readonly Item[] | undefined;
|
|
830
|
+
/** Lazily-decoded, memoized {@link DicomValue}. */
|
|
831
|
+
private _valueCache;
|
|
832
|
+
/**
|
|
833
|
+
* Construct a new structural `Element`. Producers (parser plans 02-03
|
|
834
|
+
* onward) build these directly from on-wire bytes; consumers receive
|
|
835
|
+
* them via `Dataset` after `parseDicom`.
|
|
836
|
+
*
|
|
837
|
+
* @internal
|
|
838
|
+
*/
|
|
839
|
+
constructor(init: ElementInit);
|
|
840
|
+
/**
|
|
841
|
+
* The decoded value of this element, by VR. Decode is lazy (the structural
|
|
842
|
+
* parse stays eager; field decode runs on first access) and memoized — the
|
|
843
|
+
* documented ~30× win on large studies where most fields are never read.
|
|
844
|
+
* Fail-safe: never throws; a malformed value surfaces as typed-absent with
|
|
845
|
+
* the deviation on the returned value's `warnings`.
|
|
846
|
+
*
|
|
847
|
+
* @example
|
|
848
|
+
* ```ts
|
|
849
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
850
|
+
* const ds = parseDicom(buf);
|
|
851
|
+
* const v = ds.get("00100010")?.value; // Patient's Name (PN)
|
|
852
|
+
* if (v?.kind === "personName") console.log(v.values[0]?.alphabetic.familyName);
|
|
853
|
+
* ```
|
|
854
|
+
*/
|
|
855
|
+
get value(): DicomValue;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Phase 2 structural `FileMeta` view-object shape.
|
|
860
|
+
*
|
|
861
|
+
* Per `02-CONTEXT.md` D-04 + D-17 + FM-02: a plain interface (not a class)
|
|
862
|
+
* since Phase 2 has no methods. Only `transferSyntaxUID` is required —
|
|
863
|
+
* the rest are populated by `parseFileMeta` (plan 02-02) when present
|
|
864
|
+
* but never enforced (Phase 7's `validate()` enforces FM Type-1 fields
|
|
865
|
+
* per D-19). Phase 3 may promote to a class if helpers are added.
|
|
866
|
+
*
|
|
867
|
+
* @module
|
|
868
|
+
*/
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* A non-modeled `(0002,xxxx)` File Meta element, preserved verbatim so the
|
|
872
|
+
* serializer can re-emit an exotic File Meta group byte-for-byte.
|
|
873
|
+
*
|
|
874
|
+
* The typed {@link FileMeta} fields cover the common Type-1/Type-3 elements;
|
|
875
|
+
* anything else a source file carried — e.g. `(0002,0017)`/`(0002,0018)`
|
|
876
|
+
* Sending/Receiving AE Title, `(0002,0100)` Private Information Creator UID,
|
|
877
|
+
* `(0002,0102)` Private Information — is captured here as raw bytes (the
|
|
878
|
+
* on-wire value, even-length per PS3.5 §6.2) rather than dropped. `value` is a
|
|
879
|
+
* defensive copy, so the view never aliases the parsed input buffer.
|
|
880
|
+
*/
|
|
881
|
+
interface FileMetaRawElement {
|
|
882
|
+
/** 8-char uppercase hex tag, e.g. `"00020100"`. */
|
|
883
|
+
readonly tag: Tag;
|
|
884
|
+
/** The element's Value Representation as read under Explicit VR LE. */
|
|
885
|
+
readonly vr: VR;
|
|
886
|
+
/** The raw on-wire value bytes (even-length), copied out of the input. */
|
|
887
|
+
readonly value: Buffer;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* The Part-10 File Meta Information group, projected as a typed view
|
|
891
|
+
* over `(0002,xxxx)` elements parsed during the File Meta pre-pass.
|
|
892
|
+
*
|
|
893
|
+
* Only `transferSyntaxUID` is required because it is the dispatch input
|
|
894
|
+
* for the four v1 transfer-syntax parsers; everything else is optional
|
|
895
|
+
* because real-world clinical files routinely omit one or more
|
|
896
|
+
* Type-1 elements. Phase 7's `validate()` adds opinion-bearing checks
|
|
897
|
+
* for those missing elements.
|
|
898
|
+
*
|
|
899
|
+
* @example
|
|
900
|
+
* ```ts
|
|
901
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
902
|
+
* const ds = parseDicom(buf);
|
|
903
|
+
* if (ds.fileMeta !== undefined) {
|
|
904
|
+
* const ts = ds.fileMeta.transferSyntaxUID; // always present when fileMeta is defined
|
|
905
|
+
* }
|
|
906
|
+
* ```
|
|
907
|
+
*/
|
|
908
|
+
interface FileMeta {
|
|
909
|
+
readonly transferSyntaxUID: string;
|
|
910
|
+
readonly mediaStorageSOPClassUID?: string;
|
|
911
|
+
readonly mediaStorageSOPInstanceUID?: string;
|
|
912
|
+
readonly fileMetaInformationVersion?: Buffer;
|
|
913
|
+
readonly implementationClassUID?: string;
|
|
914
|
+
readonly implementationVersionName?: string;
|
|
915
|
+
readonly sourceApplicationEntityTitle?: string;
|
|
916
|
+
/**
|
|
917
|
+
* Any `(0002,xxxx)` elements the source carried that the typed fields above
|
|
918
|
+
* do not model, preserved in tag order so a round-trip re-emits the File Meta
|
|
919
|
+
* group byte-for-byte. Omitted when the group held only modeled elements.
|
|
920
|
+
*/
|
|
921
|
+
readonly extraElements?: readonly FileMetaRawElement[];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Typed view model for the Phase 4 safety-critical domain helpers
|
|
926
|
+
* (`Dataset.patient` / `study` / `series` / `image`).
|
|
927
|
+
*
|
|
928
|
+
* Every field here obeys the §4 fail-safe contract from the dicom roadmap:
|
|
929
|
+
* a missing value is **typed-absent** (`undefined`), never a substituted
|
|
930
|
+
* default; a malformed numeric component stays `null` inside its array
|
|
931
|
+
* rather than being coerced; raw bytes are always still reachable via the
|
|
932
|
+
* underlying `Element`. These views *interpret* nothing about pixels — they
|
|
933
|
+
* surface the metadata a downstream renderer needs to interpret them
|
|
934
|
+
* correctly.
|
|
935
|
+
*
|
|
936
|
+
* The dangerous DICOM failure is the confident, wrong image — so the
|
|
937
|
+
* deliberate omissions are as load-bearing as the fields: there is no
|
|
938
|
+
* `signed` boolean unless `(0028,0103)` was actually present, no rescale
|
|
939
|
+
* slope unless `(0028,1053)` was, and the three pixel-spacing tags are
|
|
940
|
+
* three distinct fields that are never aliased.
|
|
941
|
+
*
|
|
942
|
+
* @module
|
|
943
|
+
*/
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* One entry of Other Patient IDs Sequence `(0010,1002)` — a `{id, issuer,
|
|
947
|
+
* typeCode}` triple (PS3.3 §10.15, the DICOM analogue of an HL7 v2 CX
|
|
948
|
+
* repetition). Surfaced so a caller never matches on a bare `(0010,0020)`.
|
|
949
|
+
*
|
|
950
|
+
* @example
|
|
951
|
+
* ```ts
|
|
952
|
+
* import type { OtherPatientId } from "@cosyte/dicom";
|
|
953
|
+
* const o: OtherPatientId = { id: "MRN-42", issuer: "HOSP_A", typeCode: "TEXT" };
|
|
954
|
+
* ```
|
|
955
|
+
*/
|
|
956
|
+
interface OtherPatientId {
|
|
957
|
+
readonly id?: string;
|
|
958
|
+
readonly issuer?: string;
|
|
959
|
+
readonly typeCode?: string;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Patient & study identity (§4.1 — the wrong-patient failure class).
|
|
963
|
+
*
|
|
964
|
+
* `id` is **not globally unique**; correct cross-system matching needs the
|
|
965
|
+
* `{id, issuerOfId, issuerQualifiers}` tuple plus `otherIds`. `name` keeps
|
|
966
|
+
* its full `PN` component structure, never flattened to a string.
|
|
967
|
+
*
|
|
968
|
+
* @example
|
|
969
|
+
* ```ts
|
|
970
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
971
|
+
* const p = parseDicom(buf).patient;
|
|
972
|
+
* p.id; // "MRN-42" — meaningless without the issuer
|
|
973
|
+
* p.issuerOfId; // "HOSP_A"
|
|
974
|
+
* p.name?.alphabetic.familyName; // "Doe"
|
|
975
|
+
* ```
|
|
976
|
+
*/
|
|
977
|
+
interface PatientView {
|
|
978
|
+
readonly id?: string;
|
|
979
|
+
readonly issuerOfId?: string;
|
|
980
|
+
readonly issuerQualifiers?: readonly Item[];
|
|
981
|
+
readonly otherIds: readonly OtherPatientId[];
|
|
982
|
+
readonly name?: PersonName;
|
|
983
|
+
readonly birthDate?: DicomDate;
|
|
984
|
+
readonly sex?: string;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Study-level identity (§4.1). `instanceUid` is the cross-system study key;
|
|
988
|
+
* `accessionNumber` ties the study to the order.
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```ts
|
|
992
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
993
|
+
* const s = parseDicom(buf).study;
|
|
994
|
+
* s.instanceUid; // "1.2.840.113619..."
|
|
995
|
+
* s.accessionNumber; // "ACC123"
|
|
996
|
+
* ```
|
|
997
|
+
*/
|
|
998
|
+
interface StudyView {
|
|
999
|
+
readonly instanceUid?: string;
|
|
1000
|
+
readonly id?: string;
|
|
1001
|
+
readonly accessionNumber?: string;
|
|
1002
|
+
readonly date?: DicomDate;
|
|
1003
|
+
readonly time?: DicomTime;
|
|
1004
|
+
readonly description?: string;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Series-level identity & co-registration (§4.1, §4.3). Images sharing a
|
|
1008
|
+
* `frameOfReferenceUid` are spatially co-registered.
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```ts
|
|
1012
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1013
|
+
* const s = parseDicom(buf).series;
|
|
1014
|
+
* s.modality; // "CT"
|
|
1015
|
+
* s.frameOfReferenceUid; // shared ⇒ co-registered
|
|
1016
|
+
* ```
|
|
1017
|
+
*/
|
|
1018
|
+
interface SeriesView {
|
|
1019
|
+
readonly instanceUid?: string;
|
|
1020
|
+
readonly number?: number;
|
|
1021
|
+
readonly modality?: string;
|
|
1022
|
+
readonly description?: string;
|
|
1023
|
+
readonly frameOfReferenceUid?: string;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* A coded triplet (PS3.16 §8, Table 8-1): `Code Value (0008,0100)`,
|
|
1027
|
+
* `Coding Scheme Designator (0008,0102)`, `Code Meaning (0008,0104)`. The
|
|
1028
|
+
* canonical scheme OID is resolved only for the four standard designators;
|
|
1029
|
+
* legacy SNOMED designators (`SRT`/`SNM3`/`99SDM`) deliberately resolve to
|
|
1030
|
+
* `undefined` because their code values differ from `SCT` (CP-730).
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* ```ts
|
|
1034
|
+
* import type { CodedConcept } from "@cosyte/dicom";
|
|
1035
|
+
* const c: CodedConcept = {
|
|
1036
|
+
* codeValue: "C-B1003",
|
|
1037
|
+
* codingSchemeDesignator: "SCT",
|
|
1038
|
+
* codeMeaning: "Hounsfield unit",
|
|
1039
|
+
* schemeUid: "2.16.840.1.113883.6.96",
|
|
1040
|
+
* };
|
|
1041
|
+
* ```
|
|
1042
|
+
*/
|
|
1043
|
+
interface CodedConcept {
|
|
1044
|
+
readonly codeValue?: string;
|
|
1045
|
+
readonly codingSchemeDesignator?: string;
|
|
1046
|
+
readonly codeMeaning?: string;
|
|
1047
|
+
readonly schemeUid?: string;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* A Real World Value Mapping (§4.5) — slope/intercept bound atomically to
|
|
1051
|
+
* its UCUM measurement-units code, so a number is never detached from its
|
|
1052
|
+
* units. From Real World Value Mapping Sequence `(0040,9096)`.
|
|
1053
|
+
*
|
|
1054
|
+
* @example
|
|
1055
|
+
* ```ts
|
|
1056
|
+
* import type { RealWorldValueMap } from "@cosyte/dicom";
|
|
1057
|
+
* const m: RealWorldValueMap = { slope: 1, intercept: 0, unitsCode: { codeValue: "[hnsf'U]" } };
|
|
1058
|
+
* ```
|
|
1059
|
+
*/
|
|
1060
|
+
interface RealWorldValueMap {
|
|
1061
|
+
readonly slope?: number;
|
|
1062
|
+
readonly intercept?: number;
|
|
1063
|
+
readonly unitsCode?: CodedConcept;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* The five Enhanced multi-frame functional-group macros, resolved for a
|
|
1067
|
+
* single frame Per-Frame-else-Shared (§4.4, PS3.3 §C.7.6.16). Each macro is
|
|
1068
|
+
* typed-absent when present in neither the per-frame nor the shared group
|
|
1069
|
+
* (the three *geometry* macros are treated as required for an enhanced
|
|
1070
|
+
* object — see {@link ImageView.frame}).
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```ts
|
|
1074
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1075
|
+
* const f = parseDicom(enhancedBuf).image.frame(0);
|
|
1076
|
+
* f.planePosition?.imagePositionPatient; // this frame's [x,y,z]
|
|
1077
|
+
* f.pixelMeasures?.pixelSpacing; // this frame's [row,col] mm
|
|
1078
|
+
* ```
|
|
1079
|
+
*/
|
|
1080
|
+
interface FrameFunctionalGroups {
|
|
1081
|
+
readonly index: number;
|
|
1082
|
+
readonly pixelMeasures?: {
|
|
1083
|
+
readonly pixelSpacing?: readonly (number | null)[];
|
|
1084
|
+
readonly sliceThickness?: number;
|
|
1085
|
+
readonly spacingBetweenSlices?: number;
|
|
1086
|
+
};
|
|
1087
|
+
readonly planePosition?: {
|
|
1088
|
+
readonly imagePositionPatient?: readonly (number | null)[];
|
|
1089
|
+
};
|
|
1090
|
+
readonly planeOrientation?: {
|
|
1091
|
+
readonly imageOrientationPatient?: readonly (number | null)[];
|
|
1092
|
+
};
|
|
1093
|
+
readonly pixelValueTransformation?: {
|
|
1094
|
+
readonly rescaleSlope?: number;
|
|
1095
|
+
readonly rescaleIntercept?: number;
|
|
1096
|
+
readonly rescaleType?: string;
|
|
1097
|
+
};
|
|
1098
|
+
readonly frameVoiLut?: {
|
|
1099
|
+
readonly windowCenter?: readonly (number | null)[];
|
|
1100
|
+
readonly windowWidth?: readonly (number | null)[];
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Pixel-interpretation + geometry metadata (§4.2 / §4.3 / §4.4 / §4.5) — the
|
|
1105
|
+
* "wrong pixels look fine" and "looks fine, measures wrong" classes. v1 does
|
|
1106
|
+
* not decode pixels; this view surfaces exactly what a renderer needs so it
|
|
1107
|
+
* does not have to guess.
|
|
1108
|
+
*
|
|
1109
|
+
* Safety-critical omissions are intentional: `rescaleSlope` is absent (not
|
|
1110
|
+
* `1`) when the tag is absent; `signed` is absent (not a guess) unless
|
|
1111
|
+
* `(0028,0103)` was present; `photometricInterpretation` is absent (not
|
|
1112
|
+
* `MONOCHROME2`) when absent; the three pixel-spacing fields are distinct.
|
|
1113
|
+
* When `modalityLutSequence` / `voiLutSequence` are present they are
|
|
1114
|
+
* authoritative over the linear `rescale*` / `window*` pairs.
|
|
1115
|
+
*
|
|
1116
|
+
* @example
|
|
1117
|
+
* ```ts
|
|
1118
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1119
|
+
* const img = parseDicom(buf).image;
|
|
1120
|
+
* img.rescaleSlope; // undefined ⇒ caller MUST NOT assume 1
|
|
1121
|
+
* img.signed; // undefined ⇒ signedness unknown, never guess
|
|
1122
|
+
* img.pixelSpacing; // patient-plane mm — distinct from imagerPixelSpacing
|
|
1123
|
+
* ```
|
|
1124
|
+
*/
|
|
1125
|
+
interface ImageView {
|
|
1126
|
+
readonly sopInstanceUid?: string;
|
|
1127
|
+
readonly rows?: number;
|
|
1128
|
+
readonly columns?: number;
|
|
1129
|
+
readonly samplesPerPixel?: number;
|
|
1130
|
+
readonly photometricInterpretation?: string;
|
|
1131
|
+
readonly planarConfiguration?: number;
|
|
1132
|
+
readonly bitsAllocated?: number;
|
|
1133
|
+
readonly bitsStored?: number;
|
|
1134
|
+
readonly highBit?: number;
|
|
1135
|
+
/** Raw `(0028,0103)` value: 0 = unsigned, 1 = signed. Absent ⇒ unknown. */
|
|
1136
|
+
readonly pixelRepresentation?: number;
|
|
1137
|
+
/** `true`/`false` only when `(0028,0103)` was 1/0; absent ⇒ never guessed. */
|
|
1138
|
+
readonly signed?: boolean;
|
|
1139
|
+
readonly rescaleSlope?: number;
|
|
1140
|
+
readonly rescaleIntercept?: number;
|
|
1141
|
+
readonly rescaleType?: string;
|
|
1142
|
+
readonly windowCenter?: readonly (number | null)[];
|
|
1143
|
+
readonly windowWidth?: readonly (number | null)[];
|
|
1144
|
+
readonly modalityLutSequence?: readonly Item[];
|
|
1145
|
+
readonly voiLutSequence?: readonly Item[];
|
|
1146
|
+
readonly pixelSpacing?: readonly (number | null)[];
|
|
1147
|
+
readonly imagerPixelSpacing?: readonly (number | null)[];
|
|
1148
|
+
readonly nominalScannedPixelSpacing?: readonly (number | null)[];
|
|
1149
|
+
readonly sliceThickness?: number;
|
|
1150
|
+
readonly spacingBetweenSlices?: number;
|
|
1151
|
+
readonly imagePositionPatient?: readonly (number | null)[];
|
|
1152
|
+
readonly imageOrientationPatient?: readonly (number | null)[];
|
|
1153
|
+
readonly frameOfReferenceUid?: string;
|
|
1154
|
+
readonly numberOfFrames?: number;
|
|
1155
|
+
readonly units?: string;
|
|
1156
|
+
readonly realWorldValueMaps?: readonly RealWorldValueMap[];
|
|
1157
|
+
/** `true` when this object carries Per-Frame/Shared Functional Groups. */
|
|
1158
|
+
readonly isEnhancedMultiFrame: boolean;
|
|
1159
|
+
/**
|
|
1160
|
+
* Resolve the functional-group macros for frame `index` Per-Frame-else-Shared.
|
|
1161
|
+
* Throws {@link DicomValueError} `FRAME_INDEX_OUT_OF_RANGE` for an index
|
|
1162
|
+
* outside `[0, numberOfFrames)` and `MISSING_REQUIRED_FUNCTIONAL_GROUP`
|
|
1163
|
+
* when an enhanced object lacks a required geometry macro in both groups.
|
|
1164
|
+
*/
|
|
1165
|
+
frame(index: number): FrameFunctionalGroups;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Phase 2 structural `Dataset` — top-level container produced by
|
|
1170
|
+
* `parseDicom`.
|
|
1171
|
+
*
|
|
1172
|
+
* Per `02-CONTEXT.md` D-04 + D-42: only `fileMeta` and `warnings` are
|
|
1173
|
+
* public; the element map is held internally and Phase 3 extends this
|
|
1174
|
+
* class with the `get` / `has` / `elements` / `getAll` / `setElement` /
|
|
1175
|
+
* `addElement` / `removeElement` / `addItem` / `removeItem` navigation
|
|
1176
|
+
* surface.
|
|
1177
|
+
*
|
|
1178
|
+
* The `warnings` array is frozen at the constructor boundary (mirrors
|
|
1179
|
+
* `@cosyte/hl7` sibling `model/message.ts`) so consumers receive
|
|
1180
|
+
* `readonly DicomParseWarning[]` and downstream mutation cannot escape.
|
|
1181
|
+
*
|
|
1182
|
+
* @module
|
|
1183
|
+
*/
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Initialiser shape accepted by the `Dataset` (and `Item`) constructor.
|
|
1187
|
+
*
|
|
1188
|
+
* `fileMeta` is optional because nested `Item` datasets do not carry
|
|
1189
|
+
* their own File Meta — only the root `Dataset` returned by
|
|
1190
|
+
* `parseDicom` does.
|
|
1191
|
+
*
|
|
1192
|
+
* @internal
|
|
1193
|
+
*/
|
|
1194
|
+
interface DatasetInit {
|
|
1195
|
+
readonly fileMeta?: FileMeta;
|
|
1196
|
+
readonly warnings: readonly DicomParseWarning[];
|
|
1197
|
+
readonly elements: ReadonlyMap<Tag, Element>;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* One parsed DICOM dataset — the structural shell shipped by Phase 2.
|
|
1201
|
+
*
|
|
1202
|
+
* Phase 2 public surface: `fileMeta`, `warnings`. The internal element
|
|
1203
|
+
* map is stored on `_elements` (protected) and Phase 3 promotes it to
|
|
1204
|
+
* the public `get` / `has` / `elements` / `getAll` navigation API per
|
|
1205
|
+
* D-42.
|
|
1206
|
+
*
|
|
1207
|
+
* @example
|
|
1208
|
+
* ```ts
|
|
1209
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1210
|
+
* const ds = parseDicom(buf);
|
|
1211
|
+
* console.log(ds.fileMeta?.transferSyntaxUID);
|
|
1212
|
+
* for (const w of ds.warnings) {
|
|
1213
|
+
* console.warn(w.code, w.message);
|
|
1214
|
+
* }
|
|
1215
|
+
* ```
|
|
1216
|
+
*/
|
|
1217
|
+
declare class Dataset {
|
|
1218
|
+
readonly fileMeta: FileMeta | undefined;
|
|
1219
|
+
readonly warnings: readonly DicomParseWarning[];
|
|
1220
|
+
/**
|
|
1221
|
+
* Element map, keyed by uppercase 8-hex tag. Phase 3 promotes to
|
|
1222
|
+
* public navigation surface; Phase 2 keeps it `protected` so only
|
|
1223
|
+
* subclasses (`Item`, future Phase-3 extensions) can introspect.
|
|
1224
|
+
*
|
|
1225
|
+
* @internal
|
|
1226
|
+
*/
|
|
1227
|
+
protected readonly _elements: ReadonlyMap<Tag, Element>;
|
|
1228
|
+
/** Memoised Phase 4 domain views (built once, on first access). */
|
|
1229
|
+
private _patient?;
|
|
1230
|
+
private _study?;
|
|
1231
|
+
private _series?;
|
|
1232
|
+
private _image?;
|
|
1233
|
+
/**
|
|
1234
|
+
* Construct a new structural `Dataset`. Phase 2 freezes the warnings
|
|
1235
|
+
* array at the model boundary (sibling `message.ts` lines 117–118).
|
|
1236
|
+
*
|
|
1237
|
+
* @internal
|
|
1238
|
+
*/
|
|
1239
|
+
constructor(init: DatasetInit);
|
|
1240
|
+
/**
|
|
1241
|
+
* Look up a single element by tag. Tags are normalised to 8-char
|
|
1242
|
+
* uppercase hex, so `"7fe00010"` and `"7FE00010"` resolve to the same
|
|
1243
|
+
* element. Returns `undefined` when the tag is absent.
|
|
1244
|
+
*
|
|
1245
|
+
* @example
|
|
1246
|
+
* ```ts
|
|
1247
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1248
|
+
* const ds = parseDicom(buf);
|
|
1249
|
+
* const rows = ds.get("00280010"); // Rows
|
|
1250
|
+
* if (rows?.value.kind === "numbers") console.log(rows.value.values[0]);
|
|
1251
|
+
* ```
|
|
1252
|
+
*/
|
|
1253
|
+
get(tag: Tag): Element | undefined;
|
|
1254
|
+
/**
|
|
1255
|
+
* `true` when an element with the given tag is present (case-insensitive).
|
|
1256
|
+
*
|
|
1257
|
+
* @example
|
|
1258
|
+
* ```ts
|
|
1259
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1260
|
+
* const ds = parseDicom(buf);
|
|
1261
|
+
* if (ds.has("00100010")) console.log("has Patient's Name");
|
|
1262
|
+
* ```
|
|
1263
|
+
*/
|
|
1264
|
+
has(tag: Tag): boolean;
|
|
1265
|
+
/**
|
|
1266
|
+
* All elements in this dataset, in parse (insertion) order.
|
|
1267
|
+
*
|
|
1268
|
+
* @example
|
|
1269
|
+
* ```ts
|
|
1270
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1271
|
+
* const ds = parseDicom(buf);
|
|
1272
|
+
* for (const el of ds.elements()) console.log(el.tag, el.vr);
|
|
1273
|
+
* ```
|
|
1274
|
+
*/
|
|
1275
|
+
elements(): readonly Element[];
|
|
1276
|
+
/**
|
|
1277
|
+
* All elements matching a tag as an array (never `undefined`). A dataset
|
|
1278
|
+
* holds at most one element per tag, so this returns a 0- or 1-length
|
|
1279
|
+
* array — the convenience complement of {@link Dataset.get} for callers
|
|
1280
|
+
* that prefer an always-array shape.
|
|
1281
|
+
*
|
|
1282
|
+
* @example
|
|
1283
|
+
* ```ts
|
|
1284
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1285
|
+
* const ds = parseDicom(buf);
|
|
1286
|
+
* for (const el of ds.getAll("00080060")) console.log(el.value);
|
|
1287
|
+
* ```
|
|
1288
|
+
*/
|
|
1289
|
+
getAll(tag: Tag): readonly Element[];
|
|
1290
|
+
/**
|
|
1291
|
+
* Patient-identity view (§4.1). Fail-safe typed-absent fields; `id` is
|
|
1292
|
+
* **not** globally unique — match on the `{id, issuerOfId, ...}` tuple
|
|
1293
|
+
* plus `otherIds`, never on a bare `(0010,0020)`. Memoised on first read.
|
|
1294
|
+
*
|
|
1295
|
+
* @example
|
|
1296
|
+
* ```ts
|
|
1297
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1298
|
+
* const p = parseDicom(buf).patient;
|
|
1299
|
+
* p.name?.alphabetic.familyName; // structured PN, never flattened
|
|
1300
|
+
* ```
|
|
1301
|
+
*/
|
|
1302
|
+
get patient(): PatientView;
|
|
1303
|
+
/**
|
|
1304
|
+
* Study-identity view (§4.1). `instanceUid` is the cross-system study
|
|
1305
|
+
* key; `accessionNumber` ties it to the order. Memoised on first read.
|
|
1306
|
+
*
|
|
1307
|
+
* @example
|
|
1308
|
+
* ```ts
|
|
1309
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1310
|
+
* const s = parseDicom(buf).study;
|
|
1311
|
+
* s.instanceUid; // "1.2.840.113619..."
|
|
1312
|
+
* ```
|
|
1313
|
+
*/
|
|
1314
|
+
get study(): StudyView;
|
|
1315
|
+
/**
|
|
1316
|
+
* Series-identity & co-registration view (§4.1, §4.3). A shared
|
|
1317
|
+
* `frameOfReferenceUid` means images are spatially co-registered.
|
|
1318
|
+
* Memoised on first read.
|
|
1319
|
+
*
|
|
1320
|
+
* @example
|
|
1321
|
+
* ```ts
|
|
1322
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1323
|
+
* const s = parseDicom(buf).series;
|
|
1324
|
+
* s.modality; // "CT"
|
|
1325
|
+
* ```
|
|
1326
|
+
*/
|
|
1327
|
+
get series(): SeriesView;
|
|
1328
|
+
/**
|
|
1329
|
+
* Pixel-interpretation + geometry view (§4.2–§4.5). Surfaces exactly
|
|
1330
|
+
* what a renderer needs without guessing: `rescaleSlope`/`signed`/
|
|
1331
|
+
* `photometricInterpretation` stay absent rather than defaulted, and the
|
|
1332
|
+
* three pixel-spacing tags are distinct. Memoised on first read.
|
|
1333
|
+
*
|
|
1334
|
+
* @example
|
|
1335
|
+
* ```ts
|
|
1336
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1337
|
+
* const img = parseDicom(buf).image;
|
|
1338
|
+
* img.rescaleSlope; // undefined ⇒ MUST NOT assume 1
|
|
1339
|
+
* ```
|
|
1340
|
+
*/
|
|
1341
|
+
get image(): ImageView;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Public parser entry — `parseDicom`.
|
|
1346
|
+
*
|
|
1347
|
+
* Pipeline (Phase 2 plan 02-02):
|
|
1348
|
+
* 1. Dual EMPTY_INPUT check (raw input + post-normalize) per CONTEXT D-13.
|
|
1349
|
+
* 2. {@link normalizeInput} converts `Buffer | Uint8Array | ArrayBuffer`
|
|
1350
|
+
* to a zero-copy `Buffer` view per PARSE-04.
|
|
1351
|
+
* 3. {@link parsePart10Header} resolves preamble + DICM framing per D-14.
|
|
1352
|
+
* 4. {@link parseFileMeta} parses the (always Explicit VR LE) File Meta
|
|
1353
|
+
* group per FM-01 / D-17.
|
|
1354
|
+
* 5. {@link TRANSFER_SYNTAX_PARSERS} dispatches on TS UID per D-20;
|
|
1355
|
+
* unsupported UIDs throw `UNSUPPORTED_TRANSFER_SYNTAX` carrying the
|
|
1356
|
+
* `Dictionary.uid(uid)?.name` in `err.snippet`.
|
|
1357
|
+
* 6. The chosen strategy returns a `ReadonlyMap<Tag, Element>` (Phase 2
|
|
1358
|
+
* stubs — real bodies arrive in plans 02-03 / 02-04 / 02-05).
|
|
1359
|
+
* 7. The result is assembled into a structural {@link Dataset}.
|
|
1360
|
+
*
|
|
1361
|
+
* Phase 6 (D-45) wires a source/vendor `Profile` via `ParseOptions.profile`:
|
|
1362
|
+
* its `escalations` / `suppressions` reshape Tier-2 emission at the
|
|
1363
|
+
* {@link makeEmitter} chokepoint, and its private-dictionary overlay resolves
|
|
1364
|
+
* the Implicit VR of vendor private data elements.
|
|
1365
|
+
*
|
|
1366
|
+
* @module
|
|
1367
|
+
*/
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Parse a DICOM Part 10 buffer into a structural {@link Dataset}.
|
|
1371
|
+
*
|
|
1372
|
+
* Lenient by default — recoverable deviations (missing preamble, File Meta
|
|
1373
|
+
* group-length mismatch, odd-length value, etc.) are pushed into
|
|
1374
|
+
* `ds.warnings` with stable codes from `WARNING_CODES`. Four unrecoverable
|
|
1375
|
+
* structural failures throw `DicomParseError`:
|
|
1376
|
+
*
|
|
1377
|
+
* - `EMPTY_INPUT` — empty `Buffer | Uint8Array | ArrayBuffer`.
|
|
1378
|
+
* - `NOT_DICOM_PART_10` — input lacks both `DICM` magic at offset 128 and
|
|
1379
|
+
* a recognizable `(0002,0000)` File Meta Group Length at offset 0.
|
|
1380
|
+
* - `INVALID_FILE_META` — File Meta is truncated or `(0002,0010)`
|
|
1381
|
+
* Transfer Syntax UID is missing.
|
|
1382
|
+
* - `UNSUPPORTED_TRANSFER_SYNTAX` — Transfer Syntax UID is not one of the
|
|
1383
|
+
* four v1 UIDs (`1.2.840.10008.1.2`, `…1.2.1`, `…1.2.2`, `…1.2.1.99`).
|
|
1384
|
+
*
|
|
1385
|
+
* Pass `{ strict: true }` to escalate every Tier-2 warning to a thrown
|
|
1386
|
+
* `DicomParseError` carrying the warning code.
|
|
1387
|
+
*
|
|
1388
|
+
* @example
|
|
1389
|
+
* ```ts
|
|
1390
|
+
* import { parseDicom, WARNING_CODES, DicomParseError } from "@cosyte/dicom";
|
|
1391
|
+
* import { readFileSync } from "node:fs";
|
|
1392
|
+
*
|
|
1393
|
+
* // Three input shapes (PARSE-04): Buffer, Uint8Array, ArrayBuffer.
|
|
1394
|
+
* const bytes = readFileSync("study.dcm");
|
|
1395
|
+
* const ds1 = parseDicom(bytes);
|
|
1396
|
+
* const ds2 = parseDicom(new Uint8Array(bytes));
|
|
1397
|
+
* const ds3 = parseDicom(bytes.buffer);
|
|
1398
|
+
*
|
|
1399
|
+
* // Inspect File Meta + warnings.
|
|
1400
|
+
* console.log(ds1.fileMeta?.transferSyntaxUID);
|
|
1401
|
+
* for (const w of ds1.warnings) {
|
|
1402
|
+
* if (w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE) {
|
|
1403
|
+
* console.warn("bare File Meta input at offset", w.position.byteOffset);
|
|
1404
|
+
* }
|
|
1405
|
+
* }
|
|
1406
|
+
*
|
|
1407
|
+
* // Strict mode + onWarning callback.
|
|
1408
|
+
* try {
|
|
1409
|
+
* parseDicom(bytes, {
|
|
1410
|
+
* strict: true,
|
|
1411
|
+
* onWarning: (w) => console.error(w.code, "at offset", w.position.byteOffset),
|
|
1412
|
+
* });
|
|
1413
|
+
* } catch (err) {
|
|
1414
|
+
* if (err instanceof DicomParseError) {
|
|
1415
|
+
* console.error(err.code, err.byteOffset, err.snippet);
|
|
1416
|
+
* }
|
|
1417
|
+
* }
|
|
1418
|
+
* ```
|
|
1419
|
+
*/
|
|
1420
|
+
declare function parseDicom(input: Buffer | Uint8Array | ArrayBuffer): Dataset;
|
|
1421
|
+
declare function parseDicom(input: Buffer | Uint8Array | ArrayBuffer, options: ParseOptions): Dataset;
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Phase 2 structural `Sequence` — wrapper around an SQ element's items.
|
|
1425
|
+
*
|
|
1426
|
+
* Per `02-CONTEXT.md` D-04: structural surface only — `items` and
|
|
1427
|
+
* `length`. No navigation methods. Phase 3 adds `Sequence.items[N].get(...)`
|
|
1428
|
+
* via `Item`'s `Dataset`-superclass extension per D-42.
|
|
1429
|
+
*
|
|
1430
|
+
* @module
|
|
1431
|
+
*/
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* One SQ (Sequence) element's structural body.
|
|
1435
|
+
*
|
|
1436
|
+
* `length` is the on-wire length: a real byte count for defined-length
|
|
1437
|
+
* SQ, or `0xFFFFFFFF` (4_294_967_295) when undefined-length per D-29.
|
|
1438
|
+
*
|
|
1439
|
+
* @example
|
|
1440
|
+
* ```ts
|
|
1441
|
+
* import { Sequence } from "@cosyte/dicom";
|
|
1442
|
+
* // Producers (parser plan 02-04) construct sequences as follows:
|
|
1443
|
+
* // const sq = new Sequence([item0, item1], 0xFFFFFFFF);
|
|
1444
|
+
* ```
|
|
1445
|
+
*/
|
|
1446
|
+
declare class Sequence {
|
|
1447
|
+
readonly items: readonly Item[];
|
|
1448
|
+
readonly length: number;
|
|
1449
|
+
/**
|
|
1450
|
+
* Construct a new structural `Sequence`. The `items` array is frozen
|
|
1451
|
+
* at the constructor boundary so downstream mutation cannot escape.
|
|
1452
|
+
*
|
|
1453
|
+
* @internal
|
|
1454
|
+
*/
|
|
1455
|
+
constructor(items: readonly Item[], length: number);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Fatal error taxonomy for the `@cosyte/dicom` parser pipeline.
|
|
1460
|
+
*
|
|
1461
|
+
* Phase 2 core-parser context:
|
|
1462
|
+
* - D-09 — `FATAL_CODES` is a frozen `as const` registry with EXACTLY
|
|
1463
|
+
* four codes; anything less severe MUST be a Tier-2 warning.
|
|
1464
|
+
* - D-10 — `DicomParseError` carries `code`, `byteOffset`, `snippet`
|
|
1465
|
+
* (up to 16 source bytes, space-separated lowercase hex), and an
|
|
1466
|
+
* optional `contextPath`. The thrown `Error.message` is formatted
|
|
1467
|
+
* `[CODE] msg (offset=N)` with `… in path/segments` appended when
|
|
1468
|
+
* `contextPath` is provided.
|
|
1469
|
+
*
|
|
1470
|
+
* @module
|
|
1471
|
+
*/
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Stable string codes for every Tier-3 fatal the parser may throw.
|
|
1475
|
+
*
|
|
1476
|
+
* Locked at four codes per `PROJECT.md` "Fatal errors only for unrecoverable
|
|
1477
|
+
* structural corruption": anything less severe MUST be a Tier-2 warning
|
|
1478
|
+
* (see `./warnings.ts`). Consumers narrow on `err.code` to react to
|
|
1479
|
+
* specific structural failures.
|
|
1480
|
+
*
|
|
1481
|
+
* @example
|
|
1482
|
+
* ```ts
|
|
1483
|
+
* import { parseDicom, FATAL_CODES, DicomParseError } from "@cosyte/dicom";
|
|
1484
|
+
* try {
|
|
1485
|
+
* parseDicom(Buffer.alloc(0));
|
|
1486
|
+
* } catch (err) {
|
|
1487
|
+
* if (err instanceof DicomParseError && err.code === FATAL_CODES.EMPTY_INPUT) {
|
|
1488
|
+
* // handle empty input
|
|
1489
|
+
* }
|
|
1490
|
+
* }
|
|
1491
|
+
* ```
|
|
1492
|
+
*/
|
|
1493
|
+
declare const FATAL_CODES: {
|
|
1494
|
+
readonly NOT_DICOM_PART_10: "NOT_DICOM_PART_10";
|
|
1495
|
+
readonly INVALID_FILE_META: "INVALID_FILE_META";
|
|
1496
|
+
readonly UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX";
|
|
1497
|
+
readonly EMPTY_INPUT: "EMPTY_INPUT";
|
|
1498
|
+
};
|
|
1499
|
+
/**
|
|
1500
|
+
* Discriminant type for `DicomParseError.code`. Narrowing a caught error
|
|
1501
|
+
* by this code lets consumers write exhaustive `switch` blocks (enabled
|
|
1502
|
+
* by the `switch-exhaustiveness-check` lint rule) and guarantees a
|
|
1503
|
+
* typo-free comparison against the `FATAL_CODES` registry.
|
|
1504
|
+
*
|
|
1505
|
+
* @example
|
|
1506
|
+
* ```ts
|
|
1507
|
+
* import type { FatalCode } from "@cosyte/dicom";
|
|
1508
|
+
* function describe(code: FatalCode): string {
|
|
1509
|
+
* switch (code) {
|
|
1510
|
+
* case "EMPTY_INPUT":
|
|
1511
|
+
* return "input was empty";
|
|
1512
|
+
* case "NOT_DICOM_PART_10":
|
|
1513
|
+
* return "input is not a DICOM Part 10 file";
|
|
1514
|
+
* case "INVALID_FILE_META":
|
|
1515
|
+
* return "File Meta group is missing or malformed";
|
|
1516
|
+
* case "UNSUPPORTED_TRANSFER_SYNTAX":
|
|
1517
|
+
* return "Transfer Syntax UID is not supported by v1";
|
|
1518
|
+
* }
|
|
1519
|
+
* }
|
|
1520
|
+
* ```
|
|
1521
|
+
*/
|
|
1522
|
+
type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
|
|
1523
|
+
/**
|
|
1524
|
+
* Thrown by `parseDicom` when the input violates one of the four
|
|
1525
|
+
* unrecoverable Tier-3 structural rules — or, under `{ strict: true }`,
|
|
1526
|
+
* when any Tier-2 warning is escalated through the single `emit`
|
|
1527
|
+
* chokepoint (D-35). Carries byte-offset positional context plus a short
|
|
1528
|
+
* source snippet so consumers can log actionable errors.
|
|
1529
|
+
*
|
|
1530
|
+
* Message format: `[CODE] msg (offset=N)`, with `… in a/b/c` appended
|
|
1531
|
+
* when `contextPath` is provided.
|
|
1532
|
+
*
|
|
1533
|
+
* @remarks
|
|
1534
|
+
* Snippets may contain PHI when parsing real clinical files — redact at
|
|
1535
|
+
* the call site if required by your compliance posture. The library does
|
|
1536
|
+
* not redact snippets itself.
|
|
1537
|
+
*
|
|
1538
|
+
* @example
|
|
1539
|
+
* ```ts
|
|
1540
|
+
* import { parseDicom, DicomParseError } from "@cosyte/dicom";
|
|
1541
|
+
* try {
|
|
1542
|
+
* parseDicom(buffer);
|
|
1543
|
+
* } catch (err) {
|
|
1544
|
+
* if (err instanceof DicomParseError && err.code === "NOT_DICOM_PART_10") {
|
|
1545
|
+
* // err.byteOffset, err.snippet, err.contextPath all available
|
|
1546
|
+
* }
|
|
1547
|
+
* }
|
|
1548
|
+
* ```
|
|
1549
|
+
*/
|
|
1550
|
+
declare class DicomParseError extends Error {
|
|
1551
|
+
readonly code: FatalCode;
|
|
1552
|
+
readonly byteOffset: number;
|
|
1553
|
+
readonly snippet: string;
|
|
1554
|
+
readonly contextPath: readonly string[] | undefined;
|
|
1555
|
+
/**
|
|
1556
|
+
* Construct a new `DicomParseError`. All fields except `contextPath` are
|
|
1557
|
+
* required so every thrower populates positional context per `TOL-02`.
|
|
1558
|
+
*
|
|
1559
|
+
* @internal
|
|
1560
|
+
*/
|
|
1561
|
+
constructor(code: FatalCode, message: string, byteOffset: number, snippet: string, contextPath?: readonly string[]);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* VR-aware value dispatch — the body behind `Element.value`.
|
|
1566
|
+
*
|
|
1567
|
+
* `decodeElementValue` maps an `Element`'s `vr` + raw bytes to a typed
|
|
1568
|
+
* {@link DicomValue}, applying the fail-safe rules from PS3.5 §6.2:
|
|
1569
|
+
*
|
|
1570
|
+
* - **Signedness / endianness from the VR + transfer syntax**, never
|
|
1571
|
+
* guessed (numeric VRs via `./numeric`).
|
|
1572
|
+
* - **Padding**: `UI` strips trailing NULL (`0x00`); every other string VR
|
|
1573
|
+
* strips trailing SPACE (`0x20`). A mismatched pad is tolerated and
|
|
1574
|
+
* flagged (`DICOM_UI_TRAILING_SPACE` / `DICOM_TRAILING_NULL_IN_TEXT_VR`).
|
|
1575
|
+
* - **Charset**: `LO SH UC LT ST UT PN` decode through `(0008,0005)`;
|
|
1576
|
+
* the remaining string VRs are the Default Repertoire (ASCII, decoded
|
|
1577
|
+
* Latin-1-lenient with `DICOM_NON_ASCII_IN_ASCII_VR` on stray bytes).
|
|
1578
|
+
* - **Numeric strings** `DS`/`IS` → `number | null` (never `NaN`→0).
|
|
1579
|
+
* - **Temporal** `DA`/`TM`/`DT` → tolerant typed values (warn + preserve).
|
|
1580
|
+
* - **Bulk** `OB OD OF OL OV OW UN` → raw `binary` (not interpreted in v1).
|
|
1581
|
+
*
|
|
1582
|
+
* Decode is lazy/post-parse, so any deviation surfaces on the returned
|
|
1583
|
+
* value's `warnings` (it cannot join the frozen `Dataset.warnings`).
|
|
1584
|
+
*
|
|
1585
|
+
* This module is kept OUT of `vr/index.ts` so the dispatch logic is
|
|
1586
|
+
* coverage-measured (barrels are excluded from the gate).
|
|
1587
|
+
*
|
|
1588
|
+
* @module
|
|
1589
|
+
*/
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Decode an {@link Element}'s value to a typed {@link DicomValue}. Pure +
|
|
1593
|
+
* fail-safe: it never throws and never coerces a malformed value to a
|
|
1594
|
+
* plausible-but-wrong one. Called lazily (and memoized) by `Element.value`.
|
|
1595
|
+
*
|
|
1596
|
+
* @example
|
|
1597
|
+
* ```ts
|
|
1598
|
+
* import { parseDicom } from "@cosyte/dicom";
|
|
1599
|
+
* const ds = parseDicom(buf);
|
|
1600
|
+
* const v = ds.get("00280010")?.value; // Rows (US)
|
|
1601
|
+
* if (v?.kind === "numbers") console.log(v.values[0]);
|
|
1602
|
+
* ```
|
|
1603
|
+
*/
|
|
1604
|
+
declare function decodeElementValue(element: Element): DicomValue;
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* Specific Character Set `(0008,0005)` resolution + text decoding for the
|
|
1608
|
+
* Phase 3 value decoders.
|
|
1609
|
+
*
|
|
1610
|
+
* Zero runtime deps: decoding rides on Node 22's full-ICU `TextDecoder`
|
|
1611
|
+
* (the `@cosyte/dicom` ≤3-dep ceiling keeps `iconv-lite` in reserve only if
|
|
1612
|
+
* a future multi-extension CJK case forces it).
|
|
1613
|
+
*
|
|
1614
|
+
* Term-list corrections verified against PS3.3 §C.12.1.1.2 for this build
|
|
1615
|
+
* (the easy-to-get-wrong ones):
|
|
1616
|
+
* - There is **no `ISO_IR 14`** — only `ISO_IR 13` (JIS X 0201 Katakana).
|
|
1617
|
+
* - `IR 87` / `IR 159` are **code-extension-only**: the bare `ISO_IR 87` /
|
|
1618
|
+
* `ISO_IR 159` terms do NOT exist; only `ISO 2022 IR 87` / `ISO 2022 IR
|
|
1619
|
+
* 159` are valid (so they are absent from the single-byte term list).
|
|
1620
|
+
* - **`ISO_IR 203`** (Latin-9 → ISO-8859-15) IS included.
|
|
1621
|
+
*
|
|
1622
|
+
* @module
|
|
1623
|
+
*/
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Parse a `(0008,0005)` Specific Character Set value (a `CS`, possibly
|
|
1627
|
+
* multi-valued via `\`) into its trimmed defined terms.
|
|
1628
|
+
*
|
|
1629
|
+
* The first value may be empty (the ISO-2022 "G0 starts as ISO_IR 6" rule);
|
|
1630
|
+
* it is preserved as `""` rather than dropped so {@link resolveDecoderLabel}
|
|
1631
|
+
* can apply the default-repertoire fallback.
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```ts
|
|
1635
|
+
* import { parseSpecificCharacterSet } from "@cosyte/dicom";
|
|
1636
|
+
* // bytes for "ISO 2022 IR 6\\ISO 2022 IR 87"
|
|
1637
|
+
* const terms = parseSpecificCharacterSet(buf);
|
|
1638
|
+
* ```
|
|
1639
|
+
*/
|
|
1640
|
+
declare function parseSpecificCharacterSet(bytes: Buffer): readonly string[];
|
|
1641
|
+
/**
|
|
1642
|
+
* `true` when `term` is a Specific Character Set defined term this build can
|
|
1643
|
+
* map to a decoder (the empty default-repertoire term counts as known).
|
|
1644
|
+
*
|
|
1645
|
+
* @example
|
|
1646
|
+
* ```ts
|
|
1647
|
+
* import { isKnownCharsetTerm } from "@cosyte/dicom";
|
|
1648
|
+
* isKnownCharsetTerm("ISO_IR 192"); // true
|
|
1649
|
+
* isKnownCharsetTerm("ISO_IR 14"); // false — does not exist
|
|
1650
|
+
* ```
|
|
1651
|
+
*/
|
|
1652
|
+
declare function isKnownCharsetTerm(term: string): boolean;
|
|
1653
|
+
/**
|
|
1654
|
+
* Resolve the `TextDecoder` label to use for a value, given the dataset's
|
|
1655
|
+
* Specific Character Set terms. Prefers a multibyte/extended decoder when
|
|
1656
|
+
* the term list mixes single-byte and code-extension sets; falls back to
|
|
1657
|
+
* the default repertoire (Latin-1, lenient) when no term resolves.
|
|
1658
|
+
*
|
|
1659
|
+
* @example
|
|
1660
|
+
* ```ts
|
|
1661
|
+
* import { resolveDecoderLabel } from "@cosyte/dicom";
|
|
1662
|
+
* resolveDecoderLabel(["ISO 2022 IR 6", "ISO 2022 IR 87"]); // "iso-2022-jp"
|
|
1663
|
+
* resolveDecoderLabel(undefined); // "latin1"
|
|
1664
|
+
* ```
|
|
1665
|
+
*/
|
|
1666
|
+
declare function resolveDecoderLabel(terms: readonly string[] | undefined): string;
|
|
1667
|
+
/**
|
|
1668
|
+
* Decode bytes to a string under the resolved Specific Character Set,
|
|
1669
|
+
* never throwing: an unsupported decoder label falls back to UTF-8, then to
|
|
1670
|
+
* Latin-1 (1:1, cannot fail).
|
|
1671
|
+
*
|
|
1672
|
+
* @example
|
|
1673
|
+
* ```ts
|
|
1674
|
+
* import { decodeText } from "@cosyte/dicom";
|
|
1675
|
+
* const s = decodeText(buf, ["ISO_IR 192"]); // UTF-8
|
|
1676
|
+
* ```
|
|
1677
|
+
*/
|
|
1678
|
+
declare function decodeText(bytes: Buffer, terms: readonly string[] | undefined): string;
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* `PN` (Person Name) structured decode per PS3.5 §6.2.1.1.
|
|
1682
|
+
*
|
|
1683
|
+
* A PN value has up to three component groups separated by `=` (alphabetic,
|
|
1684
|
+
* ideographic, phonetic); each group has five `^`-delimited components
|
|
1685
|
+
* (family, given, middle, prefix, suffix). Trailing empty components and
|
|
1686
|
+
* groups are dropped on the wire, so decoding fills the missing slots with
|
|
1687
|
+
* the empty string and omits absent ideographic / phonetic groups.
|
|
1688
|
+
*
|
|
1689
|
+
* @module
|
|
1690
|
+
*/
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Parse a single (already charset-decoded, pad-trimmed) PN value string into
|
|
1694
|
+
* its structured {@link PersonName} form.
|
|
1695
|
+
*
|
|
1696
|
+
* @example
|
|
1697
|
+
* ```ts
|
|
1698
|
+
* import { parsePersonName } from "@cosyte/dicom";
|
|
1699
|
+
* const pn = parsePersonName("Doe^Jane^^Dr^");
|
|
1700
|
+
* pn.alphabetic.familyName; // "Doe"
|
|
1701
|
+
* pn.alphabetic.namePrefix; // "Dr"
|
|
1702
|
+
* ```
|
|
1703
|
+
*/
|
|
1704
|
+
declare function parsePersonName(value: string): PersonName;
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* Tolerant temporal decoders for `DA` / `TM` / `DT` (PS3.5 §6.2).
|
|
1708
|
+
*
|
|
1709
|
+
* Every decoder is fail-safe: a malformed value yields `valid: false` with
|
|
1710
|
+
* the raw string preserved and the numeric fields omitted — it NEVER throws
|
|
1711
|
+
* and NEVER coerces to a plausible-but-wrong date. Tolerated legacy forms
|
|
1712
|
+
* (retired dotted `YYYY.MM.DD`, partial precision, non-standard offsets) are
|
|
1713
|
+
* decoded best-effort and flagged to the caller so it can emit the matching
|
|
1714
|
+
* warning. The raw string is the source of truth on any failure.
|
|
1715
|
+
*
|
|
1716
|
+
* @module
|
|
1717
|
+
*/
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Decode a single `DA` value. `legacy` is `true` for any tolerated
|
|
1721
|
+
* non-canonical form (retired dotted `YYYY.MM.DD`, or an unparseable value
|
|
1722
|
+
* such as `"ANONYMIZED"`), signalling the caller to emit
|
|
1723
|
+
* `DICOM_DA_LEGACY_FORMAT`.
|
|
1724
|
+
*
|
|
1725
|
+
* @example
|
|
1726
|
+
* ```ts
|
|
1727
|
+
* import { parseDate } from "@cosyte/dicom";
|
|
1728
|
+
* parseDate("20240115").value.valid; // true
|
|
1729
|
+
* parseDate("2024.01.15").legacy; // true
|
|
1730
|
+
* ```
|
|
1731
|
+
*/
|
|
1732
|
+
declare function parseDate(raw: string): {
|
|
1733
|
+
value: DicomDate;
|
|
1734
|
+
legacy: boolean;
|
|
1735
|
+
};
|
|
1736
|
+
/**
|
|
1737
|
+
* Decode a single `TM` value (max 14 bytes). Precision may be truncated from
|
|
1738
|
+
* the right (`HH`, `HHMM`, `HHMMSS`, `HHMMSS.FFFFFF`).
|
|
1739
|
+
*
|
|
1740
|
+
* @example
|
|
1741
|
+
* ```ts
|
|
1742
|
+
* import { parseTime } from "@cosyte/dicom";
|
|
1743
|
+
* parseTime("133015.5").value.fractionalSeconds; // 0.5
|
|
1744
|
+
* ```
|
|
1745
|
+
*/
|
|
1746
|
+
declare function parseTime(raw: string): {
|
|
1747
|
+
value: DicomTime;
|
|
1748
|
+
};
|
|
1749
|
+
/**
|
|
1750
|
+
* Decode a single `DT` value. `nonstandardOffset` is `true` when a UTC
|
|
1751
|
+
* offset suffix is present but malformed / out of range, signalling the
|
|
1752
|
+
* caller to emit `DICOM_DT_NONSTANDARD_OFFSET`.
|
|
1753
|
+
*
|
|
1754
|
+
* @example
|
|
1755
|
+
* ```ts
|
|
1756
|
+
* import { parseDateTime } from "@cosyte/dicom";
|
|
1757
|
+
* parseDateTime("20240115133015+0100").value.offsetMinutes; // 60
|
|
1758
|
+
* ```
|
|
1759
|
+
*/
|
|
1760
|
+
declare function parseDateTime(raw: string): {
|
|
1761
|
+
value: DicomDateTime;
|
|
1762
|
+
nonstandardOffset: boolean;
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Coded-terminology surfacing (§4.6, PS3.16 §8 Table 8-1).
|
|
1767
|
+
*
|
|
1768
|
+
* A coded concept is the triplet `Code Value (0008,0100)` /
|
|
1769
|
+
* `Coding Scheme Designator (0008,0102)` / `Code Meaning (0008,0104)`. This
|
|
1770
|
+
* module **surfaces** the triplet and resolves the canonical scheme OID for
|
|
1771
|
+
* the four standard designators — it performs **no lookup, validation, or
|
|
1772
|
+
* mapping** (deferred). Detaching a code value from its scheme is unsafe, so
|
|
1773
|
+
* the three parts always travel together.
|
|
1774
|
+
*
|
|
1775
|
+
* @module
|
|
1776
|
+
*/
|
|
1777
|
+
|
|
1778
|
+
/**
|
|
1779
|
+
* Canonical coding-scheme OIDs (PS3.16 §8). **Only** the four standard
|
|
1780
|
+
* designators are mapped. Legacy SNOMED designators (`SRT` / `SNM3` /
|
|
1781
|
+
* `99SDM`) are intentionally absent: their code values differ from `SCT`
|
|
1782
|
+
* (CP-730), so resolving them to the SNOMED OID would imply a false
|
|
1783
|
+
* equality.
|
|
1784
|
+
*
|
|
1785
|
+
* @example
|
|
1786
|
+
* ```ts
|
|
1787
|
+
* import { CODING_SCHEME_OIDS } from "@cosyte/dicom";
|
|
1788
|
+
* CODING_SCHEME_OIDS.SCT; // "2.16.840.1.113883.6.96"
|
|
1789
|
+
* ```
|
|
1790
|
+
*/
|
|
1791
|
+
declare const CODING_SCHEME_OIDS: {
|
|
1792
|
+
readonly DCM: "1.2.840.10008.2.16.4";
|
|
1793
|
+
readonly SCT: "2.16.840.1.113883.6.96";
|
|
1794
|
+
readonly UCUM: "2.16.840.1.113883.6.8";
|
|
1795
|
+
readonly LN: "2.16.840.1.113883.6.1";
|
|
1796
|
+
};
|
|
1797
|
+
/**
|
|
1798
|
+
* Resolve a coding-scheme designator to its canonical OID, or `undefined`
|
|
1799
|
+
* for any non-standard / legacy designator (including `SRT`/`SNM3`/`99SDM`,
|
|
1800
|
+
* which are NOT treated as `SCT`).
|
|
1801
|
+
*
|
|
1802
|
+
* @example
|
|
1803
|
+
* ```ts
|
|
1804
|
+
* import { codingSchemeOid } from "@cosyte/dicom";
|
|
1805
|
+
* codingSchemeOid("UCUM"); // "2.16.840.1.113883.6.8"
|
|
1806
|
+
* codingSchemeOid("SRT"); // undefined — not SCT (CP-730)
|
|
1807
|
+
* ```
|
|
1808
|
+
*/
|
|
1809
|
+
declare function codingSchemeOid(designator: string | undefined): string | undefined;
|
|
1810
|
+
/**
|
|
1811
|
+
* Read the coded triplet off a code-item dataset (e.g. one item of a Code
|
|
1812
|
+
* Sequence). Every part is independently fail-safe; `schemeUid` is the
|
|
1813
|
+
* resolved OID for the designator when standard, else `undefined`.
|
|
1814
|
+
*
|
|
1815
|
+
* @example
|
|
1816
|
+
* ```ts
|
|
1817
|
+
* import { parseDicom, readCode } from "@cosyte/dicom";
|
|
1818
|
+
* const units = parseDicom(buf).image.realWorldValueMaps?.[0]?.unitsCode;
|
|
1819
|
+
* // units?.codeValue / units?.codingSchemeDesignator / units?.codeMeaning
|
|
1820
|
+
* ```
|
|
1821
|
+
*/
|
|
1822
|
+
declare function readCode(item: Dataset): CodedConcept;
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Value-layer error taxonomy for the Phase 4 domain helpers.
|
|
1826
|
+
*
|
|
1827
|
+
* This is **separate** from the parser's four-code `FATAL_CODES` /
|
|
1828
|
+
* `DicomParseError` (locked to unrecoverable Part 10 structural corruption
|
|
1829
|
+
* — see `../../parser/errors.ts`). The §4 helpers are otherwise fail-safe:
|
|
1830
|
+
* a missing *value* is typed-absent (`undefined`), never an exception. The
|
|
1831
|
+
* helper layer throws only for a **structural contract violation** the
|
|
1832
|
+
* caller asked it to resolve and that cannot be answered safely:
|
|
1833
|
+
*
|
|
1834
|
+
* - `FRAME_INDEX_OUT_OF_RANGE` — `image.frame(i)` called with an index
|
|
1835
|
+
* outside `[0, numberOfFrames)`. Returning a guessed frame would be a
|
|
1836
|
+
* silent wrong-image, so we refuse.
|
|
1837
|
+
* - `MISSING_REQUIRED_FUNCTIONAL_GROUP` — an Enhanced multi-frame object
|
|
1838
|
+
* lacks a required geometry macro (Pixel Measures / Plane Position /
|
|
1839
|
+
* Plane Orientation) in *both* the Per-Frame and Shared groups, so the
|
|
1840
|
+
* frame cannot be spatially placed.
|
|
1841
|
+
*
|
|
1842
|
+
* @module
|
|
1843
|
+
*/
|
|
1844
|
+
/**
|
|
1845
|
+
* Stable string codes the Phase 4 helpers may throw. Narrow on
|
|
1846
|
+
* {@link DicomValueError.code} to react to a specific contract violation.
|
|
1847
|
+
*
|
|
1848
|
+
* @example
|
|
1849
|
+
* ```ts
|
|
1850
|
+
* import { VALUE_ERROR_CODES } from "@cosyte/dicom";
|
|
1851
|
+
* VALUE_ERROR_CODES.FRAME_INDEX_OUT_OF_RANGE; // "FRAME_INDEX_OUT_OF_RANGE"
|
|
1852
|
+
* ```
|
|
1853
|
+
*/
|
|
1854
|
+
declare const VALUE_ERROR_CODES: {
|
|
1855
|
+
readonly FRAME_INDEX_OUT_OF_RANGE: "FRAME_INDEX_OUT_OF_RANGE";
|
|
1856
|
+
readonly MISSING_REQUIRED_FUNCTIONAL_GROUP: "MISSING_REQUIRED_FUNCTIONAL_GROUP";
|
|
1857
|
+
};
|
|
1858
|
+
/**
|
|
1859
|
+
* Discriminant for {@link DicomValueError.code}, enabling exhaustive
|
|
1860
|
+
* `switch` narrowing (the `switch-exhaustiveness-check` lint rule).
|
|
1861
|
+
*
|
|
1862
|
+
* @example
|
|
1863
|
+
* ```ts
|
|
1864
|
+
* import type { ValueErrorCode } from "@cosyte/dicom";
|
|
1865
|
+
* function describe(code: ValueErrorCode): string {
|
|
1866
|
+
* switch (code) {
|
|
1867
|
+
* case "FRAME_INDEX_OUT_OF_RANGE":
|
|
1868
|
+
* return "frame index outside [0, numberOfFrames)";
|
|
1869
|
+
* case "MISSING_REQUIRED_FUNCTIONAL_GROUP":
|
|
1870
|
+
* return "enhanced object lacks a required geometry macro";
|
|
1871
|
+
* }
|
|
1872
|
+
* }
|
|
1873
|
+
* ```
|
|
1874
|
+
*/
|
|
1875
|
+
type ValueErrorCode = (typeof VALUE_ERROR_CODES)[keyof typeof VALUE_ERROR_CODES];
|
|
1876
|
+
/**
|
|
1877
|
+
* Thrown by the Phase 4 helpers for a structural contract violation that
|
|
1878
|
+
* cannot be answered safely (see module doc). Never carries a decoded
|
|
1879
|
+
* value, so it is safe to log without leaking PHI: the `message` is built
|
|
1880
|
+
* only from the code and structural facts (indices, tag/macro names).
|
|
1881
|
+
*
|
|
1882
|
+
* @example
|
|
1883
|
+
* ```ts
|
|
1884
|
+
* import { parseDicom, DicomValueError } from "@cosyte/dicom";
|
|
1885
|
+
* const img = parseDicom(buf).image;
|
|
1886
|
+
* try {
|
|
1887
|
+
* img.frame(9999);
|
|
1888
|
+
* } catch (err) {
|
|
1889
|
+
* if (err instanceof DicomValueError && err.code === "FRAME_INDEX_OUT_OF_RANGE") {
|
|
1890
|
+
* // handle the out-of-range request
|
|
1891
|
+
* }
|
|
1892
|
+
* }
|
|
1893
|
+
* ```
|
|
1894
|
+
*/
|
|
1895
|
+
declare class DicomValueError extends Error {
|
|
1896
|
+
readonly code: ValueErrorCode;
|
|
1897
|
+
/**
|
|
1898
|
+
* Construct a new `DicomValueError`. The `message` MUST be built only
|
|
1899
|
+
* from structural facts (never a decoded attribute value) so the error
|
|
1900
|
+
* is always safe to log.
|
|
1901
|
+
*
|
|
1902
|
+
* @internal
|
|
1903
|
+
*/
|
|
1904
|
+
constructor(code: ValueErrorCode, message: string);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* `serializeDicom` — the Phase 5 Part 10 writer (the conservative half of
|
|
1909
|
+
* Postel's Law).
|
|
1910
|
+
*
|
|
1911
|
+
* Takes a {@link Dataset} (from `parseDicom`, or hand-built) and emits a
|
|
1912
|
+
* spec-clean DICOM Part 10 byte stream:
|
|
1913
|
+
*
|
|
1914
|
+
* 1. 128-byte zero preamble + `DICM` magic (PS3.10 §7.1).
|
|
1915
|
+
* 2. File Meta group `0002`, always Explicit VR LE, with a correct
|
|
1916
|
+
* `(0002,0000)` group length — see {@link encodeFileMeta}.
|
|
1917
|
+
* 3. The dataset body, in the dataset's own transfer syntax (**no
|
|
1918
|
+
* transcode**): Implicit VR LE, Explicit VR LE/BE, or — for the Deflated
|
|
1919
|
+
* syntax — an Explicit VR LE body run through RFC 1951 raw deflate
|
|
1920
|
+
* (`zlib.deflateRawSync`), symmetric to the parser's `inflateRawSync`.
|
|
1921
|
+
*
|
|
1922
|
+
* Conservative behaviour (PS3.5): scalar values are padded to even length on
|
|
1923
|
+
* write (§6.2); retired `(gggg,0000)` group-length elements are omitted from
|
|
1924
|
+
* the dataset (§7.2); short/long-form headers are chosen by VR (§7.1.2);
|
|
1925
|
+
* sequence and encapsulated-pixel-data spans pass through byte-for-byte
|
|
1926
|
+
* (§7.5 / §A.4).
|
|
1927
|
+
*
|
|
1928
|
+
* @module
|
|
1929
|
+
*/
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Serialize a {@link Dataset} to a spec-clean DICOM Part 10 `Buffer`.
|
|
1933
|
+
*
|
|
1934
|
+
* The dataset's transfer syntax is preserved (no transcoding): pixel-data
|
|
1935
|
+
* fragments and nested sequences are written back byte-for-byte, while scalar
|
|
1936
|
+
* values are re-emitted with correct even-length padding and File Meta group
|
|
1937
|
+
* length. Pure function — the input `Dataset` is never mutated.
|
|
1938
|
+
*
|
|
1939
|
+
* **Input contract.** The writer is designed for a {@link Dataset} produced by
|
|
1940
|
+
* `parseDicom`: it relies on the parser's `Element.rawBytes` representation
|
|
1941
|
+
* (value-only for scalars and Implicit-LE defined-length `SQ`; full on-wire span
|
|
1942
|
+
* for Explicit `SQ`, undefined-length spans, encapsulated Pixel Data, and the
|
|
1943
|
+
* `UN`/CP-246 fallbacks). A hand-built `Dataset` must follow the same
|
|
1944
|
+
* convention for its bytes to be encoded correctly.
|
|
1945
|
+
*
|
|
1946
|
+
* **Round-trip scope.** `parseDicom(out)` re-reads to a dataset that is equal
|
|
1947
|
+
* over the *modeled* surface (every dataset element + the typed
|
|
1948
|
+
* {@link "../dataset/file-meta".FileMeta} fields plus any non-modeled File Meta
|
|
1949
|
+
* elements preserved on `extraElements`), not a byte-exact copy of the original
|
|
1950
|
+
* file: the 128-byte preamble is normalized to zeros, the File Meta group is
|
|
1951
|
+
* rebuilt in ascending tag order (modeled fields + `extraElements` — see
|
|
1952
|
+
* {@link encodeFileMeta}), odd-length values are padded even, and retired
|
|
1953
|
+
* `(gggg,0000)` group lengths are dropped.
|
|
1954
|
+
*
|
|
1955
|
+
* @throws {@link DicomSerializeError} with code `MISSING_TRANSFER_SYNTAX` when
|
|
1956
|
+
* the dataset has no File Meta Transfer Syntax UID, or
|
|
1957
|
+
* `UNSUPPORTED_TRANSFER_SYNTAX` when that UID is outside the v1 set.
|
|
1958
|
+
*
|
|
1959
|
+
* @example
|
|
1960
|
+
* ```ts
|
|
1961
|
+
* import { parseDicom, serializeDicom } from "@cosyte/dicom";
|
|
1962
|
+
* const ds = parseDicom(buf);
|
|
1963
|
+
* const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax
|
|
1964
|
+
* // parseDicom(out) re-reads to a structurally-equal dataset.
|
|
1965
|
+
* ```
|
|
1966
|
+
*/
|
|
1967
|
+
declare function serializeDicom(ds: Dataset): Buffer;
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Serializer error taxonomy for the Phase 5 Part 10 writer.
|
|
1971
|
+
*
|
|
1972
|
+
* Like the Phase 4 {@link "../dataset/helpers/errors".DicomValueError}, this
|
|
1973
|
+
* is **separate** from the parser's four-code `FATAL_CODES` /
|
|
1974
|
+
* `DicomParseError` (which are locked to *read*-side Part 10 structural
|
|
1975
|
+
* corruption — see `../parser/errors.ts`). The serializer throws only when
|
|
1976
|
+
* it is asked to emit a buffer it cannot make spec-clean:
|
|
1977
|
+
*
|
|
1978
|
+
* - `MISSING_TRANSFER_SYNTAX` — the `Dataset` has no `fileMeta`, or its
|
|
1979
|
+
* `fileMeta.transferSyntaxUID` is empty. The Transfer Syntax UID is the
|
|
1980
|
+
* dispatch input that decides every byte of the encoding, so there is no
|
|
1981
|
+
* safe default to fall back to.
|
|
1982
|
+
* - `UNSUPPORTED_TRANSFER_SYNTAX` — the Transfer Syntax UID is not one of
|
|
1983
|
+
* the four v1 syntaxes the writer (and parser) support. The writer never
|
|
1984
|
+
* transcodes, so it cannot emit a syntax it does not understand.
|
|
1985
|
+
*
|
|
1986
|
+
* The message is built only from the Transfer Syntax UID and structural
|
|
1987
|
+
* facts (never a decoded attribute value), so it is always safe to log.
|
|
1988
|
+
*
|
|
1989
|
+
* @module
|
|
1990
|
+
*/
|
|
1991
|
+
/**
|
|
1992
|
+
* Stable string codes the Phase 5 serializer may throw. Narrow on
|
|
1993
|
+
* {@link DicomSerializeError.code} to react to a specific failure.
|
|
1994
|
+
*
|
|
1995
|
+
* @example
|
|
1996
|
+
* ```ts
|
|
1997
|
+
* import { SERIALIZE_ERROR_CODES } from "@cosyte/dicom";
|
|
1998
|
+
* SERIALIZE_ERROR_CODES.MISSING_TRANSFER_SYNTAX; // "MISSING_TRANSFER_SYNTAX"
|
|
1999
|
+
* ```
|
|
2000
|
+
*/
|
|
2001
|
+
declare const SERIALIZE_ERROR_CODES: {
|
|
2002
|
+
readonly MISSING_TRANSFER_SYNTAX: "MISSING_TRANSFER_SYNTAX";
|
|
2003
|
+
readonly UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX";
|
|
2004
|
+
};
|
|
2005
|
+
/**
|
|
2006
|
+
* Discriminant for {@link DicomSerializeError.code}, enabling exhaustive
|
|
2007
|
+
* `switch` narrowing (the `switch-exhaustiveness-check` lint rule).
|
|
2008
|
+
*
|
|
2009
|
+
* @example
|
|
2010
|
+
* ```ts
|
|
2011
|
+
* import type { SerializeErrorCode } from "@cosyte/dicom";
|
|
2012
|
+
* function describe(code: SerializeErrorCode): string {
|
|
2013
|
+
* switch (code) {
|
|
2014
|
+
* case "MISSING_TRANSFER_SYNTAX":
|
|
2015
|
+
* return "dataset has no Transfer Syntax UID to serialize under";
|
|
2016
|
+
* case "UNSUPPORTED_TRANSFER_SYNTAX":
|
|
2017
|
+
* return "Transfer Syntax UID is outside the v1 set";
|
|
2018
|
+
* }
|
|
2019
|
+
* }
|
|
2020
|
+
* ```
|
|
2021
|
+
*/
|
|
2022
|
+
type SerializeErrorCode = (typeof SERIALIZE_ERROR_CODES)[keyof typeof SERIALIZE_ERROR_CODES];
|
|
2023
|
+
/**
|
|
2024
|
+
* Thrown by `serializeDicom` when a `Dataset`
|
|
2025
|
+
* cannot be emitted as spec-clean Part 10. Never carries a decoded value, so
|
|
2026
|
+
* it is safe to log without leaking PHI: the `message` is built only from the
|
|
2027
|
+
* code and the offending Transfer Syntax UID.
|
|
2028
|
+
*
|
|
2029
|
+
* @example
|
|
2030
|
+
* ```ts
|
|
2031
|
+
* import { parseDicom, serializeDicom, DicomSerializeError } from "@cosyte/dicom";
|
|
2032
|
+
* const ds = parseDicom(buf);
|
|
2033
|
+
* try {
|
|
2034
|
+
* serializeDicom(ds);
|
|
2035
|
+
* } catch (err) {
|
|
2036
|
+
* if (err instanceof DicomSerializeError && err.code === "MISSING_TRANSFER_SYNTAX") {
|
|
2037
|
+
* // dataset is missing the File Meta Transfer Syntax UID
|
|
2038
|
+
* }
|
|
2039
|
+
* }
|
|
2040
|
+
* ```
|
|
2041
|
+
*/
|
|
2042
|
+
declare class DicomSerializeError extends Error {
|
|
2043
|
+
readonly code: SerializeErrorCode;
|
|
2044
|
+
/**
|
|
2045
|
+
* Construct a new `DicomSerializeError`. The `message` MUST be built only
|
|
2046
|
+
* from structural facts (never a decoded attribute value) so the error is
|
|
2047
|
+
* always safe to log.
|
|
2048
|
+
*
|
|
2049
|
+
* @internal
|
|
2050
|
+
*/
|
|
2051
|
+
constructor(code: SerializeErrorCode, message: string);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* `defineProfile()` — the public factory for building immutable {@link Profile}
|
|
2056
|
+
* objects (Phase 6). Mirrors the `@cosyte/hl7` sibling's profile factory: a
|
|
2057
|
+
* validated options object in, a frozen `Profile` with a `describe()` method
|
|
2058
|
+
* out. Invalid input throws {@link ProfileDefinitionError} with an actionable
|
|
2059
|
+
* message rather than producing a half-built profile.
|
|
2060
|
+
*
|
|
2061
|
+
* A profile composes via `extends` (single parent or an array). Merge rules:
|
|
2062
|
+
* - `lineage` = parents' lineages then this name, de-duplicated in order.
|
|
2063
|
+
* - `escalations` / `suppressions` = set union of parents' and this
|
|
2064
|
+
* profile's, with the contradiction guard (a code may not be both
|
|
2065
|
+
* escalated and suppressed) applied to the *merged* result.
|
|
2066
|
+
* - `privateDictionary` = parents merged left-to-right, then this profile's
|
|
2067
|
+
* entries layered on top (child wins on a `(creator, key)` collision).
|
|
2068
|
+
* - `description` = this profile's, else the first parent that declares one.
|
|
2069
|
+
*
|
|
2070
|
+
* Zero runtime deps; immutability is enforced at the return boundary via
|
|
2071
|
+
* `Object.freeze` (top-level) over already-frozen inner maps/sets.
|
|
2072
|
+
*
|
|
2073
|
+
* @module
|
|
2074
|
+
*/
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* One vendor's private-dictionary overlay as authored: canonical
|
|
2078
|
+
* `"GGGGXXLL"` key → definition. Case-insensitive on input; normalized to
|
|
2079
|
+
* uppercase on store.
|
|
2080
|
+
*
|
|
2081
|
+
* @example
|
|
2082
|
+
* ```ts
|
|
2083
|
+
* import type { ProfilePrivateTags } from "@cosyte/dicom";
|
|
2084
|
+
* const csa: ProfilePrivateTags = {
|
|
2085
|
+
* "0029XX10": { vr: "OB", keyword: "CSAImageHeaderInfo", name: "CSA Image Header Info" },
|
|
2086
|
+
* };
|
|
2087
|
+
* ```
|
|
2088
|
+
*/
|
|
2089
|
+
type ProfilePrivateTags = Readonly<Record<string, PrivateTagDefinition>>;
|
|
2090
|
+
/**
|
|
2091
|
+
* Options accepted by {@link defineProfile}. Only `name` is required; every
|
|
2092
|
+
* other field defaults to empty. With `exactOptionalPropertyTypes`, omit an
|
|
2093
|
+
* unset key rather than passing `undefined`.
|
|
2094
|
+
*
|
|
2095
|
+
* @example
|
|
2096
|
+
* ```ts
|
|
2097
|
+
* import { defineProfile, WARNING_CODES } from "@cosyte/dicom";
|
|
2098
|
+
* const lenientCd = defineProfile({
|
|
2099
|
+
* name: "lenient-cd",
|
|
2100
|
+
* description: "Tolerant of conformance-loose archive CDs",
|
|
2101
|
+
* suppress: [WARNING_CODES.DICOM_ODD_LENGTH_VALUE_PADDED],
|
|
2102
|
+
* });
|
|
2103
|
+
* ```
|
|
2104
|
+
*/
|
|
2105
|
+
interface DefineProfileOptions {
|
|
2106
|
+
readonly name: string;
|
|
2107
|
+
readonly description?: string;
|
|
2108
|
+
readonly escalate?: readonly WarningCode[];
|
|
2109
|
+
readonly suppress?: readonly WarningCode[];
|
|
2110
|
+
readonly privateTags?: Readonly<Record<string, ProfilePrivateTags>>;
|
|
2111
|
+
readonly extends?: Profile | readonly Profile[];
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Build a frozen {@link Profile} from a validated options object.
|
|
2115
|
+
*
|
|
2116
|
+
* @example
|
|
2117
|
+
* ```ts
|
|
2118
|
+
* import { defineProfile, parseDicom } from "@cosyte/dicom";
|
|
2119
|
+
* const acme = defineProfile({
|
|
2120
|
+
* name: "acme",
|
|
2121
|
+
* description: "ACME PACS quirks",
|
|
2122
|
+
* privateTags: {
|
|
2123
|
+
* "ACME_PRIV_01": {
|
|
2124
|
+
* "0019XX10": { vr: "DS", keyword: "AcmeDose", name: "ACME Dose" },
|
|
2125
|
+
* },
|
|
2126
|
+
* },
|
|
2127
|
+
* });
|
|
2128
|
+
* const ds = parseDicom(buf, { profile: acme });
|
|
2129
|
+
* console.log(acme.describe?.());
|
|
2130
|
+
* ```
|
|
2131
|
+
*/
|
|
2132
|
+
declare function defineProfile(opts: DefineProfileOptions): Profile;
|
|
2133
|
+
|
|
2134
|
+
/**
|
|
2135
|
+
* `ProfileDefinitionError` — the single typed failure surface for
|
|
2136
|
+
* `defineProfile()`. Thrown (never returned) when profile-author input is
|
|
2137
|
+
* malformed: a bad name, an unknown option key, an unknown warning code, an
|
|
2138
|
+
* invalid VR, or a private-tag key that does not match the canonical
|
|
2139
|
+
* `GGGGxxEE` notation.
|
|
2140
|
+
*
|
|
2141
|
+
* This is distinct from the parse-time taxonomy (`DicomParseError` /
|
|
2142
|
+
* `DicomValueError` / `DicomSerializeError`) — a profile is built once, at
|
|
2143
|
+
* author time, long before any byte is parsed, so its errors never carry a
|
|
2144
|
+
* byte offset.
|
|
2145
|
+
*
|
|
2146
|
+
* @module
|
|
2147
|
+
*/
|
|
2148
|
+
/**
|
|
2149
|
+
* Thrown by `defineProfile()` when the supplied options are invalid.
|
|
2150
|
+
*
|
|
2151
|
+
* @example
|
|
2152
|
+
* ```ts
|
|
2153
|
+
* import { defineProfile, ProfileDefinitionError } from "@cosyte/dicom";
|
|
2154
|
+
* try {
|
|
2155
|
+
* defineProfile({ name: "" });
|
|
2156
|
+
* } catch (err) {
|
|
2157
|
+
* if (err instanceof ProfileDefinitionError) {
|
|
2158
|
+
* console.error(err.message); // "Profile name must be a non-empty string."
|
|
2159
|
+
* }
|
|
2160
|
+
* }
|
|
2161
|
+
* ```
|
|
2162
|
+
*/
|
|
2163
|
+
declare class ProfileDefinitionError extends Error {
|
|
2164
|
+
/**
|
|
2165
|
+
* The offending profile's `name` when known (`undefined` when the failure
|
|
2166
|
+
* is the name itself). Lets callers attribute a composed-lineage failure to
|
|
2167
|
+
* the specific profile that introduced it.
|
|
2168
|
+
*/
|
|
2169
|
+
readonly profileName?: string;
|
|
2170
|
+
/**
|
|
2171
|
+
* @param message - Actionable description of what made the options invalid.
|
|
2172
|
+
* @param profileName - The offending profile's `name`, when known.
|
|
2173
|
+
*/
|
|
2174
|
+
constructor(message: string, profileName?: string);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* Frozen namespace of every built-in profile: three vendor private-dictionary
|
|
2179
|
+
* overlays (`ge`, `siemens`, `philips`) and two posture presets (`strict`,
|
|
2180
|
+
* `lenient`). Pass one straight to `parseDicom`.
|
|
2181
|
+
*
|
|
2182
|
+
* @example
|
|
2183
|
+
* ```ts
|
|
2184
|
+
* import { parseDicom, profiles } from "@cosyte/dicom";
|
|
2185
|
+
* const ds = parseDicom(buf, { profile: profiles.siemens });
|
|
2186
|
+
* console.log(profiles.siemens.describe?.());
|
|
2187
|
+
* ```
|
|
2188
|
+
*/
|
|
2189
|
+
declare const profiles: Readonly<{
|
|
2190
|
+
ge: Profile;
|
|
2191
|
+
siemens: Profile;
|
|
2192
|
+
philips: Profile;
|
|
2193
|
+
strict: Profile;
|
|
2194
|
+
lenient: Profile;
|
|
2195
|
+
}>;
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* PS3.15 Annex E attribute-action table — DICOM Basic Application Confidentiality
|
|
2199
|
+
* Profile + 11 retention/clean option sets.
|
|
2200
|
+
*
|
|
2201
|
+
* Phase 1 deliverable (D-08 / D-09 / D-14). Consumed by Phase 7 `anonymize()`
|
|
2202
|
+
* (ANON-01..ANON-10).
|
|
2203
|
+
*
|
|
2204
|
+
* NOTE: This module is NOT re-exported from the package's `src/index.ts` (per D-10 +
|
|
2205
|
+
* D-27 — Phase 1's external surface is `Dictionary.{lookup,byKeyword,uid}` + `VERSION`
|
|
2206
|
+
* only). Phase 7 imports `annexE` via the `@cosyte/dicom/dictionary/annex-e` internal
|
|
2207
|
+
* path, which the `package.json` `exports` map will admit when Phase 7 lands its plans.
|
|
2208
|
+
*
|
|
2209
|
+
* The 9 *metadata-affecting* PS3.15 Annex E option-set columns (E.3.3–E.3.11 plus the
|
|
2210
|
+
* collapsed E.3.6 "RetainLongitudinalTemporal") populate `AnnexEAction.optionSet`
|
|
2211
|
+
* keys per attribute. The two *pixel-level* options — E.3.1 `CleanPixelData` and
|
|
2212
|
+
* E.3.2 `CleanRecognizableVisual` — are not represented per-attribute (PS3.15 Table
|
|
2213
|
+
* E.1-1 has no column for them); Phase 7 enforces them at the pixel-decode layer.
|
|
2214
|
+
* Both names remain in `AnnexEOption` for completeness and for Phase 7's API.
|
|
2215
|
+
*/
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* PS3.15 Annex E Table E.1-1 action codes.
|
|
2219
|
+
*
|
|
2220
|
+
* - `D` = replace with dummy value of compatible VR
|
|
2221
|
+
* - `Z` = replace with zero-length value
|
|
2222
|
+
* - `X` = remove element entirely
|
|
2223
|
+
* - `K` = keep original value
|
|
2224
|
+
* - `C` = clean / structured replacement per the action table
|
|
2225
|
+
* - `U` = replace UID with a consistent new UID per session
|
|
2226
|
+
*
|
|
2227
|
+
* Compound codes (`Z/D`, `X/Z/D`, `X/Z/U*`, `C/X`) are preserved verbatim from the
|
|
2228
|
+
* Annex E source; Phase 7 interprets them per the table's per-attribute semantics
|
|
2229
|
+
* (e.g. `Z/D` = `Z` if absent, `D` if present).
|
|
2230
|
+
*
|
|
2231
|
+
* @example
|
|
2232
|
+
* const code: AnnexEActionCode = "Z";
|
|
2233
|
+
*/
|
|
2234
|
+
type AnnexEActionCode = "D" | "Z" | "X" | "K" | "C" | "U" | "Z/D" | "X/Z" | "X/D" | "X/Z/D" | "X/Z/U*" | "C/X";
|
|
2235
|
+
/**
|
|
2236
|
+
* One of the 11 PS3.15 Annex E option sets (E.3.1–E.3.11).
|
|
2237
|
+
*
|
|
2238
|
+
* Names match REQUIREMENTS.md ANON-02 verbatim. `CleanPixelData` (E.3.1) and
|
|
2239
|
+
* `CleanRecognizableVisual` (E.3.2) act on pixel data, not metadata, and never
|
|
2240
|
+
* appear as `optionSet` keys in the generated `ANNEX_E` map; they remain in the
|
|
2241
|
+
* union for Phase 7's pixel-decode API.
|
|
2242
|
+
*
|
|
2243
|
+
* @example
|
|
2244
|
+
* const opt: AnnexEOption = "RetainLongitudinalTemporal";
|
|
2245
|
+
*/
|
|
2246
|
+
type AnnexEOption = "CleanPixelData" | "CleanRecognizableVisual" | "CleanGraphics" | "CleanStructuredContent" | "CleanDescriptors" | "RetainLongitudinalTemporal" | "RetainPatientCharacteristics" | "RetainDeviceIdentity" | "RetainUIDs" | "RetainSafePrivate" | "RetainInstitutionIdentity";
|
|
2247
|
+
|
|
2248
|
+
/**
|
|
2249
|
+
* Public types + error taxonomy for Phase 7 metadata de-identification
|
|
2250
|
+
* (PS3.15 Annex E).
|
|
2251
|
+
*
|
|
2252
|
+
* @module
|
|
2253
|
+
*/
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* The PS3.15 Annex E option sets `deidentify` honours — the nine
|
|
2257
|
+
* *metadata-affecting* columns of Table E.1-1. The two pixel-level options
|
|
2258
|
+
* (`CleanPixelData` §E.3.1, `CleanRecognizableVisual` §E.3.2) are deliberately
|
|
2259
|
+
* excluded: this is a metadata-only de-identifier and cannot inspect pixels
|
|
2260
|
+
* (deferred to `@cosyte/dicom-pixel`). When pixel data is present it always
|
|
2261
|
+
* warns rather than claiming the image is clean.
|
|
2262
|
+
*
|
|
2263
|
+
* @example
|
|
2264
|
+
* const retain: DeidentifyOption[] = ["RetainLongitudinalTemporal", "RetainSafePrivate"];
|
|
2265
|
+
*/
|
|
2266
|
+
type DeidentifyOption = Exclude<AnnexEOption, "CleanPixelData" | "CleanRecognizableVisual">;
|
|
2267
|
+
/**
|
|
2268
|
+
* The nine metadata option-set names, frozen for runtime validation.
|
|
2269
|
+
*
|
|
2270
|
+
* @example
|
|
2271
|
+
* ```ts
|
|
2272
|
+
* import { DEIDENTIFY_OPTIONS } from "@cosyte/dicom";
|
|
2273
|
+
* DEIDENTIFY_OPTIONS.includes("RetainUIDs"); // true
|
|
2274
|
+
* ```
|
|
2275
|
+
*/
|
|
2276
|
+
declare const DEIDENTIFY_OPTIONS: readonly DeidentifyOption[];
|
|
2277
|
+
/**
|
|
2278
|
+
* What `deidentify` actually did to one attribute — the concrete outcome of the
|
|
2279
|
+
* resolved Annex E action.
|
|
2280
|
+
*
|
|
2281
|
+
* - `removed` — the element was deleted (`X`).
|
|
2282
|
+
* - `emptied` — replaced with a zero-length value (`Z`).
|
|
2283
|
+
* - `dummied` — replaced with a non-identifying dummy of compatible VR (`D`).
|
|
2284
|
+
* - `uid-remapped` — UID(s) replaced with internally-consistent UIDs (`U`).
|
|
2285
|
+
* - `cleaned` — conservatively blanked because a safe similar-meaning value
|
|
2286
|
+
* cannot be synthesised at the metadata layer (`C`; see known limitations).
|
|
2287
|
+
* - `kept` — retained, either by an active Retain option or because the SQ was
|
|
2288
|
+
* kept and its items cleaned recursively.
|
|
2289
|
+
*
|
|
2290
|
+
* @example
|
|
2291
|
+
* ```ts
|
|
2292
|
+
* import { deidentify, parseDicom, type AppliedAction } from "@cosyte/dicom";
|
|
2293
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2294
|
+
* const removed = report.attributes.filter((a) => a.applied === ("removed" satisfies AppliedAction));
|
|
2295
|
+
* ```
|
|
2296
|
+
*/
|
|
2297
|
+
type AppliedAction = "removed" | "emptied" | "dummied" | "uid-remapped" | "cleaned" | "kept";
|
|
2298
|
+
/**
|
|
2299
|
+
* One audited attribute outcome. Carries only structural facts — tag, keyword,
|
|
2300
|
+
* the resolved Annex E action code, and the SQ context path — **never** a
|
|
2301
|
+
* decoded value, so a report is always safe to log.
|
|
2302
|
+
*
|
|
2303
|
+
* @example
|
|
2304
|
+
* ```ts
|
|
2305
|
+
* import { deidentify, parseDicom, type DeidentifiedAttribute } from "@cosyte/dicom";
|
|
2306
|
+
* const { report } = deidentify(parseDicom(buf));
|
|
2307
|
+
* report.attributes.forEach((a: DeidentifiedAttribute) => {
|
|
2308
|
+
* console.log(a.keyword, a.action, a.applied); // structural facts only — safe to log
|
|
2309
|
+
* });
|
|
2310
|
+
* ```
|
|
2311
|
+
*/
|
|
2312
|
+
interface DeidentifiedAttribute {
|
|
2313
|
+
readonly tag: Tag;
|
|
2314
|
+
readonly keyword: string;
|
|
2315
|
+
/** The resolved single action after collapsing any conditional code. */
|
|
2316
|
+
readonly action: Exclude<AnnexEActionCode, `${string}/${string}`>;
|
|
2317
|
+
readonly applied: AppliedAction;
|
|
2318
|
+
/** Tag/index chain for an attribute inside a sequence; omitted at the root. */
|
|
2319
|
+
readonly contextPath?: readonly string[];
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* The audit trail returned alongside the de-identified dataset. Contains no
|
|
2323
|
+
* decoded values — only tags, keywords, action codes, and the UID map (whose
|
|
2324
|
+
* keys/values are UIDs, not patient data).
|
|
2325
|
+
*
|
|
2326
|
+
* @example
|
|
2327
|
+
* ```ts
|
|
2328
|
+
* import { deidentify, parseDicom, type DeidentifyReport } from "@cosyte/dicom";
|
|
2329
|
+
* const { report }: { report: DeidentifyReport } = deidentify(parseDicom(buf));
|
|
2330
|
+
* console.log(report.attributes.length, "attributes acted on");
|
|
2331
|
+
* console.log(report.warnings.map((w) => w.code)); // e.g. burned-in annotation
|
|
2332
|
+
* ```
|
|
2333
|
+
*/
|
|
2334
|
+
interface DeidentifyReport {
|
|
2335
|
+
/** Per-attribute outcomes for every attribute Annex E acted on. */
|
|
2336
|
+
readonly attributes: readonly DeidentifiedAttribute[];
|
|
2337
|
+
/** Private tags removed under the Basic Profile (kept ones are omitted). */
|
|
2338
|
+
readonly removedPrivateTags: readonly Tag[];
|
|
2339
|
+
/** Source UID → replacement UID, for cross-file consistency. */
|
|
2340
|
+
readonly uidMap: ReadonlyMap<string, string>;
|
|
2341
|
+
/** Safety warnings — notably burned-in-pixel annotation that cannot be cleaned. */
|
|
2342
|
+
readonly warnings: readonly DicomParseWarning[];
|
|
2343
|
+
/** The Retain/Clean options that were active for this run. */
|
|
2344
|
+
readonly retained: readonly DeidentifyOption[];
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Options controlling a de-identification run. All optional — the default is
|
|
2348
|
+
* the Basic Application Level Confidentiality Profile with no Retain options.
|
|
2349
|
+
*
|
|
2350
|
+
* @example
|
|
2351
|
+
* ```ts
|
|
2352
|
+
* import { deidentify, parseDicom, type DeidentifyOptions } from "@cosyte/dicom";
|
|
2353
|
+
* const opts: DeidentifyOptions = { retain: ["RetainLongitudinalTemporal", "CleanDescriptors"] };
|
|
2354
|
+
* const { dataset } = deidentify(parseDicom(buf), opts);
|
|
2355
|
+
* ```
|
|
2356
|
+
*/
|
|
2357
|
+
interface DeidentifyOptions {
|
|
2358
|
+
/** Annex E option sets to activate (Retain* / Clean*). Default: none. */
|
|
2359
|
+
readonly retain?: readonly DeidentifyOption[];
|
|
2360
|
+
/** Root for generated UIDs (action `U`). Default `"2.25"`. */
|
|
2361
|
+
readonly uidRoot?: string;
|
|
2362
|
+
/**
|
|
2363
|
+
* A caller-owned source→replacement UID cache. Pass one shared map across a
|
|
2364
|
+
* whole study/archive to make UID remapping consistent by construction even
|
|
2365
|
+
* across separate calls (it is consistent anyway — the mapping is content-
|
|
2366
|
+
* derived — but a shared map also makes repeats O(1)).
|
|
2367
|
+
*/
|
|
2368
|
+
readonly uidMap?: Map<string, string>;
|
|
2369
|
+
/**
|
|
2370
|
+
* A Phase 6 {@link Profile} whose private-dictionary overlay names the
|
|
2371
|
+
* known-safe private attributes to keep when `RetainSafePrivate` is active.
|
|
2372
|
+
* Without it, `RetainSafePrivate` keeps nothing (fail-safe).
|
|
2373
|
+
*/
|
|
2374
|
+
readonly profile?: Profile;
|
|
2375
|
+
/**
|
|
2376
|
+
* Text written to `(0012,0063)` De-identification Method. Default names the
|
|
2377
|
+
* Basic Profile and the active options.
|
|
2378
|
+
*/
|
|
2379
|
+
readonly deidentificationMethod?: string;
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* The result of {@link deidentify}: a new dataset plus its audit report.
|
|
2383
|
+
*
|
|
2384
|
+
* @example
|
|
2385
|
+
* ```ts
|
|
2386
|
+
* import { deidentify, parseDicom, serializeDicom, type DeidentifyResult } from "@cosyte/dicom";
|
|
2387
|
+
* const { dataset, report }: DeidentifyResult<ReturnType<typeof parseDicom>> = deidentify(parseDicom(buf));
|
|
2388
|
+
* const safe = serializeDicom(dataset); // input dataset is never mutated
|
|
2389
|
+
* void report;
|
|
2390
|
+
* ```
|
|
2391
|
+
*/
|
|
2392
|
+
interface DeidentifyResult<TDataset> {
|
|
2393
|
+
readonly dataset: TDataset;
|
|
2394
|
+
readonly report: DeidentifyReport;
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Stable codes for {@link DeidentifyError}.
|
|
2398
|
+
*
|
|
2399
|
+
* @example
|
|
2400
|
+
* ```ts
|
|
2401
|
+
* import { DEIDENTIFY_ERROR_CODES } from "@cosyte/dicom";
|
|
2402
|
+
* DEIDENTIFY_ERROR_CODES.INVALID_OPTIONS; // "INVALID_OPTIONS"
|
|
2403
|
+
* ```
|
|
2404
|
+
*/
|
|
2405
|
+
declare const DEIDENTIFY_ERROR_CODES: Readonly<{
|
|
2406
|
+
readonly INVALID_OPTIONS: "INVALID_OPTIONS";
|
|
2407
|
+
}>;
|
|
2408
|
+
/**
|
|
2409
|
+
* One of the {@link DEIDENTIFY_ERROR_CODES} values.
|
|
2410
|
+
*
|
|
2411
|
+
* @example
|
|
2412
|
+
* ```ts
|
|
2413
|
+
* import { DeidentifyError, type DeidentifyErrorCode } from "@cosyte/dicom";
|
|
2414
|
+
* const code: DeidentifyErrorCode = "INVALID_OPTIONS";
|
|
2415
|
+
* throw new DeidentifyError("unknown retain option", code);
|
|
2416
|
+
* ```
|
|
2417
|
+
*/
|
|
2418
|
+
type DeidentifyErrorCode = (typeof DEIDENTIFY_ERROR_CODES)[keyof typeof DEIDENTIFY_ERROR_CODES];
|
|
2419
|
+
/**
|
|
2420
|
+
* Thrown for an author-time misconfiguration of {@link deidentify} (an unknown
|
|
2421
|
+
* Retain option, a malformed UID root). Distinct from the parser's fatal codes,
|
|
2422
|
+
* the value layer's `DicomValueError`, and the serializer's `DicomSerializeError`.
|
|
2423
|
+
* The message carries only structural facts (option names, the UID root) — never
|
|
2424
|
+
* a decoded value.
|
|
2425
|
+
*
|
|
2426
|
+
* @example
|
|
2427
|
+
* ```ts
|
|
2428
|
+
* import { deidentify, DeidentifyError } from "@cosyte/dicom";
|
|
2429
|
+
* try {
|
|
2430
|
+
* // @ts-expect-error — not a valid option
|
|
2431
|
+
* deidentify(ds, { retain: ["RetainEverything"] });
|
|
2432
|
+
* } catch (e) {
|
|
2433
|
+
* if (e instanceof DeidentifyError) console.error(e.code); // "INVALID_OPTIONS"
|
|
2434
|
+
* }
|
|
2435
|
+
* ```
|
|
2436
|
+
*/
|
|
2437
|
+
declare class DeidentifyError extends Error {
|
|
2438
|
+
readonly code: DeidentifyErrorCode;
|
|
2439
|
+
/**
|
|
2440
|
+
* @param message Human-readable, PHI-free description.
|
|
2441
|
+
* @param code One of {@link DEIDENTIFY_ERROR_CODES}.
|
|
2442
|
+
*/
|
|
2443
|
+
constructor(message: string, code: DeidentifyErrorCode);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
/**
|
|
2447
|
+
* `deidentify` — PS3.15 Annex E metadata-level de-identification.
|
|
2448
|
+
*
|
|
2449
|
+
* Applies the **Basic Application Level Confidentiality Profile** plus any of the
|
|
2450
|
+
* nine *metadata-affecting* Annex E Options, driven by the generated Table E.1-1
|
|
2451
|
+
* action map ({@link annexE}). It is a **pure** function: the input {@link Dataset}
|
|
2452
|
+
* is never mutated; a fresh `Dataset` (with a rebuilt element map and File Meta)
|
|
2453
|
+
* is returned alongside a value-free {@link DeidentifyReport}.
|
|
2454
|
+
*
|
|
2455
|
+
* **What it does**
|
|
2456
|
+
* - Resolves each attribute's action (basic profile, overridden by an active
|
|
2457
|
+
* Retain/Clean Option), collapsing conditional codes to their leftmost branch
|
|
2458
|
+
* (see {@link resolveAction}), and applies it: `X` remove, `Z` zero-length, `D`
|
|
2459
|
+
* VR-consistent dummy (falling back to `Z` where no safe dummy exists), `C`
|
|
2460
|
+
* conservative blank, `U` deterministic consistent-UID remap, `K` keep.
|
|
2461
|
+
* - Recurses into kept sequences and **re-encodes** them so nested PHI is removed
|
|
2462
|
+
* in the *serialized* bytes too — not just the object model (the Phase 5 writer
|
|
2463
|
+
* blits `SQ` spans verbatim, so a rebuilt `items` array alone would not survive
|
|
2464
|
+
* serialization). Rebuilt sequences are normalized to defined length.
|
|
2465
|
+
* - Removes all private attributes by default; with `RetainSafePrivate` + a
|
|
2466
|
+
* {@link Profile}, keeps the private data elements the profile's overlay names
|
|
2467
|
+
* as safe (and the private-creator elements the profile recognizes).
|
|
2468
|
+
* - Remaps `(0002,0003)` Media Storage SOP Instance UID consistently (unless
|
|
2469
|
+
* `RetainUIDs`), writes `(0012,0062)` Patient Identity Removed = `YES` and
|
|
2470
|
+
* `(0012,0063)` De-identification Method, and warns
|
|
2471
|
+
* (`DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED`) when Pixel Data is present and not
|
|
2472
|
+
* marked free of burned-in annotation — this metadata-only pass cannot clean
|
|
2473
|
+
* pixels (deferred to `@cosyte/dicom-pixel`).
|
|
2474
|
+
*
|
|
2475
|
+
* **Known limitations** (documented, fail-safe toward *more* removal):
|
|
2476
|
+
* - No IOD Type-1 conformance analysis, so conditional codes always take the
|
|
2477
|
+
* most-protective leftmost branch — a Type-1 attribute that strictly needed a
|
|
2478
|
+
* dummy is instead removed/emptied.
|
|
2479
|
+
* - `C` (clean) is a conservative blank, not a meaning-preserving structured
|
|
2480
|
+
* replacement (which needs domain context the metadata layer lacks).
|
|
2481
|
+
* - Pixel-level options (`CleanPixelData`, `CleanRecognizableVisual`) are out of
|
|
2482
|
+
* scope; burned-in text is warned, never cleaned.
|
|
2483
|
+
* - A private data element kept under `RetainSafePrivate` is kept *verbatim* — if
|
|
2484
|
+
* it is itself a sequence carrying standard PHI attributes, that nested content
|
|
2485
|
+
* is not recursed. The profile vouches the element is safe; nest accordingly.
|
|
2486
|
+
* - A sequence whose `items` the parser did not materialize (e.g. an
|
|
2487
|
+
* undefined-length `UN` value stored as an opaque span) is kept verbatim rather
|
|
2488
|
+
* than recursed, so any nested listed attributes are not de-identified.
|
|
2489
|
+
*
|
|
2490
|
+
* @module
|
|
2491
|
+
*/
|
|
2492
|
+
|
|
2493
|
+
/**
|
|
2494
|
+
* De-identify a {@link Dataset} per PS3.15 Annex E — the Basic Application Level
|
|
2495
|
+
* Confidentiality Profile, plus any Retain/Clean Options passed in `retain`.
|
|
2496
|
+
*
|
|
2497
|
+
* Pure: `ds` is never mutated. Returns a fresh dataset and a value-free
|
|
2498
|
+
* {@link DeidentifyReport} (tags, keywords, action codes, the UID map, warnings).
|
|
2499
|
+
*
|
|
2500
|
+
* @throws {@link DeidentifyError} (`INVALID_OPTIONS`) for an unknown Retain option
|
|
2501
|
+
* or a malformed `uidRoot`.
|
|
2502
|
+
*
|
|
2503
|
+
* @example
|
|
2504
|
+
* ```ts
|
|
2505
|
+
* import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom";
|
|
2506
|
+
* const { dataset, report } = deidentify(parseDicom(buf));
|
|
2507
|
+
* const clean = serializeDicom(dataset); // safe to share
|
|
2508
|
+
* console.log(report.attributes.length, "attributes acted on");
|
|
2509
|
+
* ```
|
|
2510
|
+
*/
|
|
2511
|
+
declare function deidentify(ds: Dataset, options?: DeidentifyOptions): DeidentifyResult<Dataset>;
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* Deterministic UID remapping for PS3.15 Annex E action `U` ("replace with an
|
|
2515
|
+
* internally-consistent UID").
|
|
2516
|
+
*
|
|
2517
|
+
* The replacement is a pure function of the source UID and the chosen root:
|
|
2518
|
+
* `sha256(sourceUid)` reduced to a decimal string under `<root>.…`. Because it
|
|
2519
|
+
* is content-derived, the *same* source UID maps to the *same* replacement
|
|
2520
|
+
* across separate `deidentify` calls and across files in a study set — without
|
|
2521
|
+
* any shared state — so cross-instance referential integrity (Study ⇄ Series ⇄
|
|
2522
|
+
* SOP, Frame of Reference, referenced instances) is preserved. A per-call cache
|
|
2523
|
+
* `Map` makes repeats within one call O(1) and lets a caller thread one map
|
|
2524
|
+
* through a whole archive if they prefer explicit sharing.
|
|
2525
|
+
*
|
|
2526
|
+
* Default root `2.25` is the DICOM-sanctioned UUID-derived arc (PS3.5 §B.2) —
|
|
2527
|
+
* a globally-unique root that needs no registration.
|
|
2528
|
+
*
|
|
2529
|
+
* @module
|
|
2530
|
+
*/
|
|
2531
|
+
/**
|
|
2532
|
+
* DICOM `2.25` UUID-derived root (PS3.5 §B.2) — no registration required.
|
|
2533
|
+
*
|
|
2534
|
+
* @example
|
|
2535
|
+
* ```ts
|
|
2536
|
+
* import { makeUidRemapper, DEFAULT_UID_ROOT } from "@cosyte/dicom";
|
|
2537
|
+
* const remap = makeUidRemapper(DEFAULT_UID_ROOT);
|
|
2538
|
+
* remap.map("1.2.840.113619.2.55.3").startsWith("2.25."); // true
|
|
2539
|
+
* ```
|
|
2540
|
+
*/
|
|
2541
|
+
declare const DEFAULT_UID_ROOT = "2.25";
|
|
2542
|
+
/**
|
|
2543
|
+
* A UID remapper: a stable `map(src)` plus the backing cache it fills.
|
|
2544
|
+
*
|
|
2545
|
+
* @example
|
|
2546
|
+
* ```ts
|
|
2547
|
+
* import { makeUidRemapper, type UidRemapper } from "@cosyte/dicom";
|
|
2548
|
+
* const remap: UidRemapper = makeUidRemapper();
|
|
2549
|
+
* const replaced = remap.map("1.2.840.10008.5.1.4.1.1.2");
|
|
2550
|
+
* remap.cache.get("1.2.840.10008.5.1.4.1.1.2") === replaced; // true
|
|
2551
|
+
* ```
|
|
2552
|
+
*/
|
|
2553
|
+
interface UidRemapper {
|
|
2554
|
+
/** Map one source UID to its deterministic replacement (cached). */
|
|
2555
|
+
readonly map: (sourceUid: string) => string;
|
|
2556
|
+
/** The source→replacement cache, exposed for reporting / reuse. */
|
|
2557
|
+
readonly cache: Map<string, string>;
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Build a {@link UidRemapper} rooted at `root`, optionally seeded with (and
|
|
2561
|
+
* writing through to) a caller-owned `cache` for cross-call sharing.
|
|
2562
|
+
*
|
|
2563
|
+
* @param root Dotted-decimal UID root (default {@link DEFAULT_UID_ROOT}).
|
|
2564
|
+
* @param cache Source→replacement map to fill (default a fresh `Map`).
|
|
2565
|
+
* @throws {DeidentifyError} when `root` is not a valid dotted-decimal OID prefix
|
|
2566
|
+
* or leaves no room for a value component within the 64-char UID limit.
|
|
2567
|
+
*
|
|
2568
|
+
* @example
|
|
2569
|
+
* ```ts
|
|
2570
|
+
* import { makeUidRemapper } from "@cosyte/dicom";
|
|
2571
|
+
* const remap = makeUidRemapper();
|
|
2572
|
+
* remap.map("1.2.840.113619.2.55.3") === remap.map("1.2.840.113619.2.55.3"); // true
|
|
2573
|
+
* ```
|
|
2574
|
+
*/
|
|
2575
|
+
declare function makeUidRemapper(root?: string, cache?: Map<string, string>): UidRemapper;
|
|
2576
|
+
|
|
2577
|
+
export { type AppliedAction, CODING_SCHEME_OIDS, type CodedConcept, DEFAULT_UID_ROOT, DEIDENTIFY_ERROR_CODES, DEIDENTIFY_OPTIONS, Dataset, type DefineProfileOptions, type DeidentifiedAttribute, DeidentifyError, type DeidentifyErrorCode, type DeidentifyOption, type DeidentifyOptions, type DeidentifyReport, type DeidentifyResult, type DicomDate, type DicomDateTime, DicomParseError, type DicomParseWarning, type DicomPosition, DicomSerializeError, type DicomTime, type DicomValue, DicomValueError, index as Dictionary, Element, FATAL_CODES, type FatalCode, type FileMeta, type FileMetaRawElement, type FrameFunctionalGroups, type ImageView, Item, type OnWarningCallback, type OtherPatientId, type ParseOptions, type PatientView, type PersonName, type PersonNameGroup, type PrivateTagDefinition, type Profile, ProfileDefinitionError, type ProfilePrivateTags, type RealWorldValueMap, SERIALIZE_ERROR_CODES, Sequence, type SerializeErrorCode, type SeriesView, type StudyView, type UidRemapper, VALUE_ERROR_CODES, VERSION, type ValueErrorCode, WARNING_CODES, type WarningCode, codingSchemeOid, decodeElementValue, decodeText, defineProfile, deidentify, isKnownCharsetTerm, makeUidRemapper, parseDate, parseDateTime, parseDicom, parsePersonName, parseSpecificCharacterSet, parseTime, profiles, readCode, resolveDecoderLabel, serializeDicom };
|