@cosyte/dicom 0.0.4 → 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 +276 -182
- package/README.md +24 -14
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
|
@@ -314,7 +324,7 @@ The 4 Tier-3 fatal codes (`NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED
|
|
|
314
324
|
|
|
315
325
|
## Error Handling
|
|
316
326
|
|
|
317
|
-
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.
|
|
318
328
|
|
|
319
329
|
### `DicomParseError`
|
|
320
330
|
|
|
@@ -350,13 +360,13 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
|
|
|
350
360
|
|
|
351
361
|
`@cosyte/dicom` is metadata-first by design. Even at v1-complete, do **not** rely on it for:
|
|
352
362
|
|
|
353
|
-
- **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`.
|
|
354
364
|
- **Burned-in PHI.** v1 **warns** it cannot remove burned-in annotation; a "de-identified" output is **metadata-de-identified only**.
|
|
355
365
|
- **Networking & web.** No DIMSE (C-STORE/FIND/MOVE, MWL, MPPS); no DICOMweb (QIDO/WADO/STOW). → `@cosyte/dicom-net`, `@cosyte/dicomweb`.
|
|
356
366
|
- **Transcoding.** No transfer-syntax conversion. The serializer re-emits in the dataset's source syntax only.
|
|
357
367
|
- **Terminology resolution.** Coded values are surfaced (designator + canonical source) but not validated against SNOMED/LOINC/etc.
|
|
358
368
|
|
|
359
|
-
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.
|
|
360
370
|
|
|
361
371
|
---
|
|
362
372
|
|