Cookbook
Task-oriented recipes for the DICOM jobs you actually get handed. Each one is: here's the problem,
here's the code, here's what you get back. Every symbol below is a real @cosyte/dicom export, no
pseudo-API. All sample objects are synthetic (an invented patient, fake UIDs), encoded as base64
so a recipe needs no file on disk; never paste a real DICOM object into a doc or a test.
Read Getting started first for the parse model; the recipes here assume you can already
get a parsed Dataset.
1. Re-serialize a parsed object to spec-clean bytes
The problem: you parsed an object (perhaps a quirky one) and need Part 10 bytes back out for storage or forwarding, with a guarantee that nothing was silently lost.
serializeDicom(ds) writes a Dataset back to a Part 10 Buffer: preamble + DICM, File Meta
always Explicit VR LE with a recomputed group length, and the dataset body in the source transfer
syntax, no transcode. It obeys the conservative half of Postel's Law: even-length padding,
correct headers, byte-for-byte sequence and encapsulated-pixel-data passthrough. Serializing is a
fixed point.
import { parseDicom, serializeDicom } from "@cosyte/dicom";
const buf = Buffer.from(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMjAyNDAxMTUIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg",
"base64",
);
const ds = parseDicom(buf);
const out = serializeDicom(ds);
Buffer.isBuffer(out); // => true
// Re-parsing the bytes yields the same data...
parseDicom(out).patient.name?.alphabetic?.familyName; // => "Doe"
// ...and re-serializing is byte-identical (a fixed point).
serializeDicom(parseDicom(out)).equals(out); // => true
Only the typed FileMeta fields round-trip through the model; the rest of File Meta is recomputed
spec-clean on emit.
2. De-identify before sharing
The problem: you need to strip identifying metadata before an object leaves your control, and you need a record of what was done, without mutating the original.
deidentify(ds) applies the PS3.15 Annex E Basic Application Level Confidentiality Profile
(replacing, emptying, or removing every attribute the standard lists as identifying) and returns a
fresh Dataset plus a value-free DeidentifyReport. It is a pure function: your input
dataset is never mutated.
import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom";
const buf = Buffer.from(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMjAyNDAxMTUIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg",
"base64",
);
const ds = parseDicom(buf);
const { dataset, report } = deidentify(ds);
// The original is untouched; the identifying fields are gone from the copy.
ds.patient.id; // => "MRN-42"
dataset.patient.id; // => undefined
// Patient's Name is emptied (Annex E action "Z"), not left in place.
dataset.get("00100010")?.value.kind; // => "empty"
// UIDs are remapped to deterministic 2.25 replacements that stay consistent across files.
dataset.study.instanceUid?.startsWith("2.25."); // => true
// The report lists what was acted on: value-free, safe to log.
report.attributes.length > 0; // => true
report.warnings.length; // => 0
// The de-identified copy serializes to bytes you can share.
Buffer.isBuffer(serializeDicom(dataset)); // => true
This is metadata-level de-identification. Pixel data is out of scope: when an object carries
burned-in annotation this layer cannot remove, you get a DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED
warning on the report rather than a false sense of safety. Pixel cleaning is deferred to
@cosyte/dicom-pixel. Opt into any of the nine metadata-affecting Annex E Options (e.g. RetainUIDs,
RetainLongitudinalTemporal, CleanDescriptors) via deidentify(ds, { retain: [...] }).
The attribute-action table behind all of this is generated from NEMA's PS3.15 2026c DocBook, the
normative publication of the standard, rather than from a third-party mirror of it. That matters
because an attribute the table does not list is an attribute deidentify() keeps, silently: the
current edition's patient attributes, including the (0010,00xx) preferred-name and pronoun block,
the (0010,004x) gender-identity and sex-parameters attributes, and EthnicGroupCodeSequence /
EthnicGroups, are all removed because the table is the edition's, not a snapshot of it. Four rows
of Table E.1-1 name a family rather than a single tag ((50xx,xxxx) Curve Data, (60xx,3000) and
(60xx,4000) Overlay Data and Comments, and the odd-group private-attribute row). All four are
acted on: private attributes are removed through their own path, and the three repeating-group rows
are matched by mask across the sixteen even groups PS3.5 bounds them to, removed, and reported with
the mask that matched:
const { report } = deidentify(parseDicom(buf));
report.attributes
.filter((a) => a.repeatingGroup !== undefined)
.forEach((a) => console.log(a.tag, a.keyword, a.repeatingGroup));
// 60004000 Overlay Comments 60xx4000
3. Read raw pixel data without decoding it
The problem: you need the pixel bytes (to hand to an imaging pipeline, to hash, to forward) but
@cosyte/dicom deliberately does not decode them.
Pixel Data (7FE0,0010) decodes to a { kind: "binary", bytes } value: the raw Buffer, exactly
as stored. You read the geometry from the image view and the bytes from the
element; interpreting them is your pipeline's job.
import { parseDicom } from "@cosyte/dicom";
// Synthetic 2x2, 16-bit object with a tiny raw Pixel Data element.
const buf = Buffer.from(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xACgAEABVUwIAAgAoABEAVVMCAAIAKAAAAVVTAgAQAOB/EABPQgAACAAAAAEAAgADAAQA",
"base64",
);
const ds = parseDicom(buf);
ds.image.rows; // => 2
ds.image.columns; // => 2
const pixels = ds.get("7FE00010")?.value; // Pixel Data
pixels?.kind; // => "binary"
const byteLength = pixels?.kind === "binary" ? pixels.bytes.length : undefined;
byteLength; // => 8
ds.warnings.length; // => 0
The bytes are never windowed, rescaled, or color-transformed here. That is out of scope. For encapsulated (compressed) transfer syntaxes the value exposes the raw fragments, still undecoded.
4. Triage warnings: the lenient, never-throw contract
The problem: you want to log or triage every tolerated deviation without your pipeline throwing on a vendor quirk.
Every recoverable deviation collects on ds.warnings with a stable code and a byte offset; only the
four fatal conditions throw. The messages are PHI-free by construction, so the whole array is safe to
log.
import { parseDicom, WARNING_CODES } from "@cosyte/dicom";
// Synthetic object with the preamble omitted: a tolerated quirk.
const buf = Buffer.from(
"AgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAYABDUwIAQ1QQACAATE8GAE1STi00Mg==",
"base64",
);
const ds = parseDicom(buf);
// The object parsed; the deviation is recorded, not hidden.
ds.series.modality; // => "CT"
ds.warnings.some((w) => w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE); // => true
// Every warning carries a stable code and a byte offset: safe to log, no PHI.
ds.warnings.every((w) => typeof w.code === "string"); // => true
Escalate when you want strictness. A source profile can promote chosen
warning codes to a thrown DicomParseError (a spec-conformance gate for a trusted sender) or
suppress benign, high-volume codes for a known-quirky source.