@cosyte/dicom 0.0.3 → 0.0.5
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 +417 -70
- package/README.md +28 -14
- package/dist/index.cjs +641 -190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.mjs +641 -190
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
|
|
3
|
+
<img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
1
6
|
# @cosyte/dicom
|
|
2
7
|
|
|
3
8
|
> Read a real-world, vendor-quirky DICOM Part 10 file and pull the metadata you need in one line, without having read the DICOM standard.
|
|
@@ -26,7 +31,7 @@ import { parseDicom } from "@cosyte/dicom";
|
|
|
26
31
|
|
|
27
32
|
const ds = parseDicom(await readFile("study.dcm"));
|
|
28
33
|
|
|
29
|
-
ds.get("
|
|
34
|
+
ds.get("00100010")?.value; // (0010,0010) Patient's Name: structured PN, never flattened
|
|
30
35
|
ds.study.instanceUid; // "1.2.840.…": the global study anchor
|
|
31
36
|
ds.image.rescaleSlope; // number | undefined: undefined means "absent", never 1
|
|
32
37
|
```
|
|
@@ -38,7 +43,7 @@ That's the pitch: no config, no schema upload, no spec lookup. The parser accept
|
|
|
38
43
|
## Features
|
|
39
44
|
|
|
40
45
|
- **One-line metadata extraction**: `ds.patient`, `ds.study`, `ds.series`, `ds.image`: typed, fail-safe views over the safety-critical attributes. No `(group,element)` tags to memorise.
|
|
41
|
-
- **Two access patterns**: named views, or structural `ds.get("
|
|
46
|
+
- **Two access patterns**: named views, or structural `ds.get("00100010")` by 8-character `(group,element)` tag (resolve a keyword to its tag with `Dictionary.byKeyword`), plus `ds.elements()` to walk everything.
|
|
42
47
|
- **Lazy typed value decode**: `element.value` decodes raw bytes into a discriminated `DicomValue` across all 34 VRs (numbers, `bigint`s, person names, dates/times, sequences, raw `binary`), honoring `(0008,0005)` Specific Character Set through nested items.
|
|
43
48
|
- **Real-world tolerance, Postel's Law**: a lenient reader emits 24 stable warning codes for what it tolerated; only 4 truly-structural conditions are fatal. The serializer always writes spec-clean Part 10.
|
|
44
49
|
- **Source/vendor profile system**: `defineProfile()` + 5 built-ins (`ge`, `siemens`, `philips`, `strict`, `lenient`) that only ever _tighten or annotate_ a parse, resolving vendor private tags by the file's live Private Creator string, never a wrong decode.
|
|
@@ -87,15 +92,20 @@ s.instanceUid; // "1.2.840.…" Study Instance UID (0020,000D)
|
|
|
87
92
|
s.accessionNumber; // ties the study to the HIS order (0008,0050)
|
|
88
93
|
```
|
|
89
94
|
|
|
90
|
-
### By
|
|
95
|
+
### By tag
|
|
91
96
|
|
|
92
|
-
`get`
|
|
97
|
+
`get`, `has` and `getAll` take the **8-character `(group,element)` tag** (case-insensitive) and only that: `"00080060"`, not `"Modality"` and not `"(0008,0060)"`. A keyword resolves to its tag through the dictionary first. `getAll` is the always-array complement of `get` (a dataset holds at most one element per tag, so it returns 0 or 1), and `elements()` walks everything.
|
|
93
98
|
|
|
94
99
|
```ts
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ds.
|
|
100
|
+
import { Dictionary } from "@cosyte/dicom";
|
|
101
|
+
|
|
102
|
+
ds.get("00080060"); // Modality (0008,0060)
|
|
103
|
+
ds.has("7FE00010"); // boolean: is Pixel Data present
|
|
98
104
|
ds.elements(); // readonly Element[]: walk everything
|
|
105
|
+
|
|
106
|
+
// Prefer keywords? Resolve one to its tag, then get by tag.
|
|
107
|
+
const tag = Dictionary.byKeyword("Modality")?.tag; // "00080060"
|
|
108
|
+
ds.get(tag ?? "");
|
|
99
109
|
```
|
|
100
110
|
|
|
101
111
|
### Typed values
|
|
@@ -103,10 +113,10 @@ ds.elements(); // readonly Element[]: walk everything
|
|
|
103
113
|
`get` returns an `Element`; its `.value` lazily decodes the raw bytes into a discriminated `DicomValue` and caches the result.
|
|
104
114
|
|
|
105
115
|
```ts
|
|
106
|
-
const rows = ds.get("
|
|
116
|
+
const rows = ds.get("00280010")?.value; // Rows, a US
|
|
107
117
|
if (rows?.kind === "numbers") rows.values[0]; // 512
|
|
108
118
|
|
|
109
|
-
const name = ds.get("
|
|
119
|
+
const name = ds.get("00100010")?.value; // Patient's Name, a PN
|
|
110
120
|
if (name?.kind === "personName") name.values[0]?.alphabetic.givenName; // "Jane"
|
|
111
121
|
```
|
|
112
122
|
|
|
@@ -141,7 +151,7 @@ async function indexFile(path: string) {
|
|
|
141
151
|
}
|
|
142
152
|
```
|
|
143
153
|
|
|
144
|
-
|
|
154
|
+
A quirky object is tolerated rather than rejected, and absent fields come back `undefined`. Check `ds.warnings` to log what was tolerated. A folder walk **does** still need a `try`/`catch`, because all four Tier-3 conditions throw and a real archive meets all four: `UNSUPPORTED_TRANSFER_SYNTAX` for a pixel-compressed object, which this parser does not read; `INVALID_FILE_META` for a truncated or partly-copied file; `NOT_DICOM_PART_10` for whatever non-DICOM file wandered into the folder; and `EMPTY_INPUT` for a zero-byte one. They all throw the one class, so catch `DicomParseError` per file and skip.
|
|
145
155
|
|
|
146
156
|
### Build routing keys
|
|
147
157
|
|
|
@@ -173,7 +183,7 @@ img.photometricInterpretation; // (0028,0004) never defaulted to MONOCHROME2
|
|
|
173
183
|
img.pixelSpacing; // (0028,0030) patient-plane mm, distinct from imagerPixelSpacing
|
|
174
184
|
```
|
|
175
185
|
|
|
176
|
-
> **Vendor note.** Philips writes private rescale tags `(2005,1409/140A/140B)` that shadow the standard `(0028,1052/1053)`; using the standard tags alone can yield non-quantitative values. This parser **preserves** the private tags so you can prefer them. Reach them with `ds.get("
|
|
186
|
+
> **Vendor note.** Philips writes private rescale tags `(2005,1409/140A/140B)` that shadow the standard `(0028,1052/1053)`; using the standard tags alone can yield non-quantitative values. This parser **preserves** the private tags so you can prefer them. Reach them with `ds.get("20051409")` (optionally under `profiles.philips`).
|
|
177
187
|
|
|
178
188
|
For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame-else-Shared (PS3.3 C.7.6.16). It throws a `DicomValueError` (carrying only structural facts, never PHI) for an out-of-range frame or a required geometry macro missing from both groups.
|
|
179
189
|
|
|
@@ -210,6 +220,10 @@ deidentify(parseDicom(buf), {
|
|
|
210
220
|
|
|
211
221
|
This is **metadata-level** de-identification. Pixel cleaning is out of scope: when a file carries burned-in annotation this layer cannot remove, you get a `DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` warning rather than a false sense of safety (pixel cleaning is deferred to `@cosyte/dicom-pixel`).
|
|
212
222
|
|
|
223
|
+
The action table comes from NEMA's PS3.15 2026c DocBook, the normative publication of the standard, rather than from a third-party mirror of it, so the current edition's patient attributes are removed rather than quietly kept. That includes the three rows the standard states as a repeating-group mask rather than a single tag: `(50xx,xxxx)` Curve Data, `(60xx,3000)` Overlay Data and `(60xx,4000)` Overlay Comments are matched in every overlay or curve group the standard defines, removed, and named in the report with the mask that matched them. Overlay comments in particular are a common carrier for text typed onto a study, so a clean report on a file that still held them was worse than no report.
|
|
224
|
+
|
|
225
|
+
The groups a mask covers are the sixteen even ones PS3.5 bounds it to (`6000`-`601E`, `5000`-`501E`), not any four hex digits. Reading `xx` as a wildcard would strip attributes the standard never marked, which is data loss on a call you asked to be conservative. That bound is read out of PS3.5 itself, pinned by SHA-256 the same way the action table is, rather than copied into the source by hand: the current edition states the overlay range, and the curve range comes from the 2004 edition its own note delegates to, with the two required to agree where they overlap.
|
|
226
|
+
|
|
213
227
|
### Bridge to FHIR / HL7 v2
|
|
214
228
|
|
|
215
229
|
A common consulting ask is joining imaging to the rest of the record. The authoritative crosswalk is the FHIR [`ImagingStudy` "Mappings for DICOM"](https://build.fhir.org/imagingstudy-mappings.html) tab. The join keys a metadata parser must surface correctly:
|
|
@@ -310,7 +324,7 @@ The 4 Tier-3 fatal codes (`NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED
|
|
|
310
324
|
|
|
311
325
|
## Error Handling
|
|
312
326
|
|
|
313
|
-
The library throws
|
|
327
|
+
The library throws five typed errors, all exported from the package barrel. Warnings are data rather than throws unless you ask otherwise: a profile's `escalate` list promotes only the codes it names.
|
|
314
328
|
|
|
315
329
|
### `DicomParseError`
|
|
316
330
|
|
|
@@ -346,13 +360,13 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
|
|
|
346
360
|
|
|
347
361
|
`@cosyte/dicom` is metadata-first by design. Even at v1-complete, do **not** rely on it for:
|
|
348
362
|
|
|
349
|
-
- **Pixel data.** No decode/decompression
|
|
363
|
+
- **Pixel data.** No decode/decompression, no rendering, no measurements: Pixel Data is exposed as raw bytes. And v1 does not read a **compressed object at all**, not even structurally: a transfer syntax outside the four listed below is the fatal `UNSUPPORTED_TRANSFER_SYNTAX`, so JPEG / JPEG-LS / JPEG2000 / RLE / HTJ2K objects do not parse. → `@cosyte/dicom-pixel`.
|
|
350
364
|
- **Burned-in PHI.** v1 **warns** it cannot remove burned-in annotation; a "de-identified" output is **metadata-de-identified only**.
|
|
351
365
|
- **Networking & web.** No DIMSE (C-STORE/FIND/MOVE, MWL, MPPS); no DICOMweb (QIDO/WADO/STOW). → `@cosyte/dicom-net`, `@cosyte/dicomweb`.
|
|
352
366
|
- **Transcoding.** No transfer-syntax conversion. The serializer re-emits in the dataset's source syntax only.
|
|
353
367
|
- **Terminology resolution.** Coded values are surfaced (designator + canonical source) but not validated against SNOMED/LOINC/etc.
|
|
354
368
|
|
|
355
|
-
Supported transfer syntaxes
|
|
369
|
+
Supported transfer syntaxes, and **exactly** these four (**pixels never decoded** in any of them): Implicit VR LE `1.2.840.10008.1.2`, Explicit VR LE `…1.2.1`, Deflated Explicit VR LE `…1.2.1.99`, Explicit VR BE `…1.2.2` (retired, legacy-only). Any other UID, which includes every pixel-compressed syntax (JPEG, JPEG-LS, JPEG2000, RLE, HTJ2K), is rejected by `parseDicom` with the fatal `UNSUPPORTED_TRANSFER_SYNTAX` rather than read structurally. Deflated is the one compressed syntax in the supported set: it deflates the whole dataset stream rather than the pixels, and it is inflated on parse.
|
|
356
370
|
|
|
357
371
|
---
|
|
358
372
|
|