Skip to main content
Version: v0.1.0

The object model: Part 10 framing, File Meta, dataset

A DICOM Part 10 object on disk is a 128-byte preamble, the DICM magic, a File Meta Information group (always Explicit VR Little Endian) that names the transfer syntax, and then the dataset: a flat, ordered list of data elements encoded in that transfer syntax. parseDicom frames all of it into one immutable Dataset.

What a data element is​

Every element is a (group,element) tag, a VR (Value Representation: the two-letter type code), a length, and a value. @cosyte/dicom keys elements by the 8-character uppercase hex tag (e.g. "00100010" for Patient's Name). The parser supports the four native transfer syntaxes, Implicit VR LE, Explicit VR LE, Explicit VR BE, and Deflated Explicit VR LE, plus every encapsulation syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest), whose Data Set section A.4 makes Explicit VR LE: those are read and written, their pixels are never decoded, and serializeDicom refuses with INVALID_ENCAPSULATED_PIXEL_DATA a top-level Pixel Data section A.4 does not allow. It also reads the four JPIP Referenced syntaxes of sections A.6, A.7, A.11 and A.12, whose Data Set is Explicit VR LE (deflated for A.7 and A.12, and inflated on parse) and whose pixels are referenced by the Pixel Data Provider URL (0028,7FE0), returned as written and never fetched; those are read only, and serializeDicom and deidentify both refuse them. In Implicit VR the on-wire VR is absent and is resolved from the dictionary; in Explicit VR the on-wire VR is honored and a disagreement with the dictionary is flagged (DICOM_VR_MISMATCH), never silently overridden.

Reaching elements​

ds.get(tag) returns the Element at a tag (or undefined); ds.has(tag) tests presence; ds.getAll(tag) is the always-array complement of get, and because a Dataset holds at most one element per tag it returns 0 or 1. All three take the tag form. get does not take a keyword. Resolve a keyword to its tag through the generated dictionary:

import { parseDicom, Dictionary } from "@cosyte/dicom";

const buf = Buffer.from(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg",
"base64",
);

const ds = parseDicom(buf);

// File Meta names the transfer syntax the dataset was encoded in.
ds.fileMeta?.transferSyntaxUID; // => "1.2.840.10008.1.2.1"

// Elements are keyed by (group,element) tag, case-insensitively.
// This synthetic object carries no Pixel Data, so that tag is absent.
ds.has("00100010"); // => true
ds.has("7FE00010"); // => false

// A keyword resolves to its tag through the dictionary; `get` then takes the tag.
Dictionary.byKeyword("PatientName")?.tag; // => "00100010"
Dictionary.lookup("00080060")?.keyword; // => "Modality"

A DICOMDIR's Directory Record tree​

When an object's File Meta (0002,0002) is 1.2.840.10008.1.3.10 (Media Storage Directory Storage), ds.directory is a DicomDirectory; for anything else it is undefined. Its records are every Item of the Directory Record Sequence (0004,1220) in order, each a DirectoryRecord with its index, its (0004,1430) type, its (0004,1500) referencedFileId components (verbatim), its item for the record keys, and its lowerLevel records. root is the record (0004,1200) names and each (0004,1400) successor; a record's lowerLevel is the record its (0004,1420) names and each successor. Each record is in the tree at most once.

import { parseDicom } from "@cosyte/dicom";

const dir = parseDicom(buf).directory;
for (const patient of dir?.root ?? []) {
for (const study of patient.lowerLevel) {
for (const series of study.lowerLevel) {
for (const image of series.lowerLevel) {
console.log(patient.type, image.referencedFileId?.join("/"));
}
}
}
}

An offset names a record only when it is exactly where that record's (FFFE,E000) Item tag sits in the file, counted from the first byte of the File Preamble, which is Item.fileOffset and how PS3.3's Basic Directory IOD defines these offsets (PS3.3 is not vendored here, so no clause is claimed for it). A file read without a preamble still counts the 132 bytes it lacks. Anything else, an offset inside a record, on an Item of a Sequence nested in a record, on the Sequence's header or past the end, names nothing and raises DICOM_DIRECTORY_OFFSET_UNRESOLVED. The limits, what the writer refuses, and what de-identification leaves undone are on Known limitations.

The generated data dictionary​

The Dictionary namespace is generated at build time from the official DICOM Part 6 source and committed, so lookups are in-memory and deterministic: no runtime network or filesystem access. Dictionary.lookup accepts either a tag or a keyword; byKeyword is keyword-only; uid resolves a UID (e.g. a transfer syntax) to its human-readable name. Unknown input returns undefined. The dictionary never throws.

The element registry, 5,310 tags, comes from NEMA's PS3.6 2026d DocBook, the normative publication of the standard, rather than from a third-party mirror of it. That is what the name, keyword, vr, vm, and retired fields on an entry are: the values PS3.6 prints for that tag, in the edition named above. retired in particular is worth reading rather than ignoring, in both directions. (0010,2160) EthnicGroup is retired and its replacements EthnicGroupCodeSequence and EthnicGroups are what current instances carry; (3004,0012) DoseValue is not retired, whatever an older dictionary may tell you. A retired entry stays resolvable, because files in the wild outlive the editions that defined them; you are told what the tag is and that it is no longer current.

import { Dictionary } from "@cosyte/dicom";

// Retired in PS3.6 2025a, and still resolvable so an older study can be read.
Dictionary.lookup("00102160")?.keyword; // => "EthnicGroup"
Dictionary.lookup("00102160")?.retired; // => true

// Its replacements, which current instances carry.
Dictionary.byKeyword("EthnicGroupCodeSequence")?.tag; // => "00102161"
Dictionary.byKeyword("EthnicGroups")?.vm; // => "1-n"

// PS3.6 still defines this one. The RET marker belongs to (3004,0010).
Dictionary.lookup("30040012")?.keyword; // => "DoseValue"
Dictionary.lookup("30040012")?.retired; // => false
Dictionary.lookup("30040010")?.retired; // => true

UID names come from the same edition, out of PS3.6 Annex A: Table A-1 (UID Values) and Table A-2 (Well-known Frames of Reference). That is every UID the registry publishes, transfer syntaxes, SOP and Meta SOP Classes, well-known SOP Instances, coding schemes and the rest, current and retired alike, rather than a hand-picked subset. Two things are deliberately not PS3.6's spelling, and both are conveniences rather than corrections:

  • Retirement is the retired boolean, not a suffix in the name. Every retired UID in Annex A carries (Retired) at the end of its UID Name; here that moves into a field you can branch on, and the name stays a name.
  • Four transfer syntaxes keep the short form every toolkit prints. PS3.6 gives them a trailing : Default Transfer Syntax for ... clause recording which storage class defaults to them, so you get Implicit VR Little Endian rather than Implicit VR Little Endian: Default Transfer Syntax for DICOM. The other four hundred odd names are the normative text, unchanged.

Two rows are absent on purpose: PS3.6 retired them and withdrew their names in the same edition, so there is no name to return and uid reports them as unknown rather than answering with an empty string.

import { Dictionary } from "@cosyte/dicom";

// A transfer syntax the current edition defines.
Dictionary.uid("1.2.840.10008.1.2.4.203")?.name; // => "High-Throughput JPEG 2000 Image Compression"
Dictionary.uid("1.2.840.10008.1.2.4.203")?.type; // => "TransferSyntax"

// The short form, not PS3.6's longer name.
Dictionary.uid("1.2.840.10008.1.2")?.name; // => "Implicit VR Little Endian"

// Retirement is a field, and the name is left alone.
Dictionary.uid("1.2.840.10008.1.2.2")?.name; // => "Explicit VR Big Endian"
Dictionary.uid("1.2.840.10008.1.2.2")?.retired; // => true

Immutability​

A Dataset is immutable at the model boundary: warnings is frozen, and the element map is not exposed for mutation. Edits go through explicit methods (setElement, addElement, removeElement, and the sequence-item equivalents), each returning results rather than mutating shared parser output. This is the same discipline the serializer relies on. See Re-serializing.

Where values come from​

ds.get(tag) gives you the raw Element; its .value decodes the bytes into a typed DicomValue. For the safety-critical attributes there is a shorter, typed path (the patient / study / series / image views) which is what the Quickstart uses.