Skip to content

Repository files navigation

tagscope

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

tagscope.dev explaining a CT header

tagscope explain and diff

Install

npm install tagscope

Node 20 or newer. No runtime dependencies.

On the web

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.

Command line

tagscope show <file> [--private] [--json]
tagscope diff <a> <b> [--ignore Keyword,0008,0018] [--skip-private] [--json]
tagscope explain <tag|keyword>
  • show prints one aligned row per element; sequence items are indented under their sequence. Private tags are hidden unless --private is given.
  • diff prints + for added, - for removed and ~ for changed elements, and descends into sequences item by item. --ignore takes keywords and tags mixed.
  • explain looks a tag up in the dictionary: tagscope explain PixelSpacing or tagscope explain 0028,0030.

Every command takes --json for scripts.

Library

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' }],
  }),
);

API

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.

What it reads

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

Design notes

  • Headers only. Pixel data is skipped unless you ask for it, which keeps show instant 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.

Development

npm install
npm test
npm run typecheck
npm run build

Credits

  • 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.

License

MIT © Ivan Novikov

About

Read, explain and diff DICOM headers from TypeScript or the command line.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages