Read, explain and diff DICOM headers — from TypeScript or the command line.
tagscope is a small, dependency-free DICOM Part 10 header reader. It is for the everyday questions: what is in this file, what does this tag mean, and why do these two files from the same series differ. It does not decode pixels.
$ tagscope diff scanner-a.dcm scanner-b.dcm --ignore SOPInstanceUID
~ (0008,0060) Modality: MR → CT
~ (0010,0010) Patient's Name: John Doe → Jane Doe
+ (0008,1030) Study Description: LSPINE
- ReferencedSeriesSequence[0] (0020,000E) Series Instance UID: 1.2.3.9
npm install tagscopeNode 20 or newer. No runtime dependencies.
tagscope.dev runs the same library in a Cloudflare Worker: drop a file, get the table. Only the header is parsed; pixel data is skipped and nothing but an optional shared explanation (rows, not the file) is stored, in D1, for 30 days. worker/ holds the Worker and site/ the page.
tagscope show <file> [--private] [--json]
tagscope diff <a> <b> [--ignore Keyword,0008,0018] [--skip-private] [--json]
tagscope explain <tag|keyword>showprints one aligned row per element; sequence items are indented under their sequence. Private tags are hidden unless--privateis given.diffprints+for added,-for removed and~for changed elements, and descends into sequences item by item.--ignoretakes keywords and tags mixed.explainlooks a tag up in the dictionary:tagscope explain PixelSpacingortagscope explain 0028,0030.
Every command takes --json for scripts.
import { readFileSync } from 'node:fs';
import { parseDicom, explainDataset, diffDatasets, renderChanges } from 'tagscope';
const a = parseDicom(new Uint8Array(readFileSync('a.dcm')));
const b = parseDicom(new Uint8Array(readFileSync('b.dcm')));
console.log(explainDataset(a.dataset).find((row) => row.keyword === 'PatientName')?.value);
console.log(renderChanges(diffDatasets(a.dataset, b.dataset, { skipPrivate: true })));Build test fixtures in code instead of shipping binary files:
import { buildDataset, writeDicom } from 'tagscope';
const bytes = writeDicom(
buildDataset({
SOPClassUID: '1.2.840.10008.5.1.4.1.1.4',
SOPInstanceUID: '1.2.3.4.5',
PatientName: 'Doe^John',
Rows: 512,
ReferencedSeriesSequence: [{ SeriesInstanceUID: '1.2.3.9' }],
}),
);| Function | What it does |
|---|---|
parseDicom(bytes, { includePixelData }) |
Reads a Part 10 file into { meta, dataset, transferSyntax }. Stops before pixel data by default. |
explainDataset(dataset) |
One { tag, keyword, name, vr, value, private, depth } row per element, sequences flattened. |
diffDatasets(a, b, { ignore, skipPrivate }) |
Added, removed and changed elements, with a path through sequences. |
displayValue(element) |
The value as a person reads it: names reordered, dates and times punctuated. |
lookup(tag), lookupKeyword(keyword) |
Dictionary entries: VR, VM, keyword and name. |
buildDataset(values), writeDicom(dataset) |
Build a dataset from keywords and write it as Explicit VR Little Endian. |
| Transfer syntaxes | Explicit VR Little Endian, Implicit VR Little Endian, and the header of any compressed syntax (pixel data is skipped) |
| Lengths | defined and undefined, including nested sequences |
| Big endian | refused with a clear error — it has been retired from the standard |
| Truncated files | fail loudly with the offset, never return half a header |
- Headers only. Pixel data is skipped unless you ask for it, which keeps
showinstant on multi-gigabyte enhanced objects. - One number per tag. Tags are packed as
group << 16 | element, so sorting and set membership are plain number operations. - A dictionary for people. The bundled dictionary is a curated subset of PS3.6 — the elements people actually look at — rather than all four thousand. Unknown tags still read; they are just called Unknown Tag.
npm install
npm test
npm run typecheck
npm run build- The data dictionary is generated from the DICOM standard's Part 6 (NEMA), via the XML that innolitics/dicom-standard publishes.
- David Clunie's dicom3tools and pydicom were my reference whenever a file looked wrong.
- Thanks to everyone who sent broken files in issues #7, #15 and #22 — most of the edge cases in the reader come from them.
MIT © Ivan Novikov

