Skip to content

v0.30.0

Latest

Choose a tag to compare

@xbmlz xbmlz released this 01 Sep 02:35
· 1 commit to main since this release
1c9ce5b

Exported mutable state is gone. The data dictionary and the UID registry were both
reachable as exported maps — read-only by convention, mutable by type, with no
synchronisation of any kind — and both are behind functions now. Seven breaking
changes, all of that one shape, and the dictionary became pluggable on the way.

The dictionary is an interface

Dictionary is one method, and Standard() returns the dictionary PS3.6 defines:

entry, ok := godicom.Standard().Lookup(tag.PixelData, "")

The creator argument names the Private Creator of a private tag and is ignored
for a standard one, so one method answers for both halves of the dictionary. It is
also the first public way to read a standard entry's VM, name or retired flag:
LookupVR gave the VR and laundered a missing entry into UN, and the
PrivateDictEntry functions covered only private tags.

Because it is an interface, a caller can bring their own. NewPrivateDictionary
builds one that belongs to a single read rather than to the process, and
NewDictionary composes — first match wins, so the argument order is the
precedence:

vendor := godicom.NewPrivateDictionary()
if err := vendor.Add("ACME 3.2", godicom.NewTag(0x0041, 0x1001), godicom.VRUS, "Some Number"); err != nil {
	return err
}
ds, err := godicom.ReadFile("ct.dcm", &godicom.ReadOptions{
	Dictionary: godicom.NewDictionary(vendor, godicom.Standard()),
})

That changes what an element means, not merely how it is described: an implicit
VR file carries no VRs, so every element's VR is whatever the dictionary says, and
for a private element that answer depends on the vendor who wrote the file. The
dataset retains the dictionary, because a deferred value is decoded on Get long
after the read returned. Closes #71.

DicomDictionaryGo, RepeatersDictionaryGo and PrivateDictionaries are gone
from the API, along with PrivateDictEntry. DictEntry stays — it is what
Lookup returns — and gained VRs(), which splits the compound forms PS3.6
writes as prose: thirty-seven entries permit two VRs, and (0028,1200) permits
three.

Deleted with them: the generated tagToKeyword and tagToName maps, 10,380 lines
between them, both a second copy of data the dictionary already held. tagToName
had no reader at all.

The UID registry

uid.Lookup changed direction. It takes a UID and returns what the registry
records about it; the keyword direction is uid.LookupKeyword:

info, ok := uid.Lookup(uid.CTImageStorage)     // Name, Keyword, Type, Retired, the transfer-syntax flags
u, ok := uid.LookupKeyword("CTImageStorage")   // what Lookup used to do

uid.Dictionary, uid.KeywordToUID and uid.Known are gone from the API, as are
the root aliases godicom.UIDDictionary and godicom.KnownUIDs.
uid.Known[uid.ImplicitVRLittleEndian] = … was one assignment away from making
every implicit-VR file in the process decode as explicit; that is why the maps
went, not tidiness.

uid.Known was also a second copy of the UID dictionary — one Info per
registered UID, all 496 of them built at program start whether anything asked or
not — and every field in it is either read off the dictionary entry or derived
from the UID in three comparisons. Lookup derives them on demand, so a program
that never looks up a UID now does no init work for one.

Six UIDs pydicom does not carry

PS3.6 Table A-1 registers them and pydicom's _uid_dict.py does not have them, so
godicom — a 1:1 mirror of that dictionary — did not either:
CTImageStorageForProcessing, EnhancedCTImageStorageForProcessing,
LegacyConvertedEnhancedCTImageStorageForProcessing,
WaveformPresentationStateStorage, WaveformAcquisitionPresentationStateStorage
and UltrasoundWaveformStorage.

They are supplied from a STANDARD_ADDITIONS table that refuses to generate once
pydicom defines any of them, so the two cannot silently drift. #77 reported the
two Waveform Presentation State classes; audit_uid_dict.py, which diffs the
generated dictionary against PS3.6 Tables A-1 and A-2, found the other four.

Private blocks

Dataset.PrivateBlock returns (*PrivateBlock, bool). It used to return a bare
pointer that was nil when the creator was not there — the ordinary case for a
file another manufacturer wrote, not an exceptional one:

// Before: a nil dereference on every file GE did not write.
ds.PrivateBlock(0x0019, "GEMS_ACQU_01").Get(0x01)

// Now:
block, ok := ds.PrivateBlock(0x0019, "GEMS_ACQU_01")

group is uint16 and offset is uint8, where both were int. A block offset
is the low byte of an element number and nothing else, so GetTag(0x1234) used to
return (0009,2234) — silently a different vendor's block — and GetTag(-1)
returned (0009,0FFF), which is not a private element at all. The type carries
the constraint now, so the check has nowhere left to fail.

Writing private data works at all for the first time: NewPrivateBlock reserves
the lowest free block and writes the Private Creator as LO, which is what a
private element needs in order to be attributable to anyone.
Dataset.PrivateCreators lists the vendors a group names — where to start with a
file whose documentation you do not have — and PrivateBlock.Delete completes the
set Get and Set started. Part of #51 §18.

Two bugs came out of that work:

  • PrivateBlock chose between two blocks the same creator reserved by map
    iteration order, so the tag a private element was read from could differ between
    runs of one program over one file. The lowest block wins now
  • a cached block outlived the element that reserved it. Deleting a Private Creator,
    or overwriting it with another vendor's name, left the block still answering with
    its old base element: reads came back from whatever now occupies those tags, and
    writes produced private elements attributed to a vendor the dataset no longer
    names. pydicom fixed the deletion half in its issue #1097 and still caches across
    the overwrite

Upgrading

Before Now
uid.Lookup("CTImageStorage") uid.LookupKeyword("CTImageStorage")
uid.Known[u] uid.Lookup(u)
uid.Dictionary[u], godicom.UIDDictionary uid.Lookup(u)
uid.KeywordToUID[k], godicom.KnownUIDs uid.LookupKeyword(k)
DicomDictionaryGo[t], RepeatersDictionaryGo godicom.Standard().Lookup(t, "")
PrivateDictionaries[creator][t] godicom.Standard().Lookup(t, creator)
block := ds.PrivateBlock(g, c) block, ok := ds.PrivateBlock(g, c)

godicom.LookupUID is unchanged and still takes a keyword, and godicom.UIDInfo
still names uid.Info. AddPrivateDictEntry and ResetExtraPrivateDictionaries
are not deprecated either — registering a vendor's blocks at startup is what they
are for — but they now document that they mutate process-global state, and point
at NewPrivateDictionary for the version that does not.

Also in this release: the CLI's usage text documents -no-meta, -top, -t and
-tag, which it never did as show grew them, and -top's description no longer
claims to change the default output — it narrows a -t search to the top level and
is a no-op without one.

Full changelog: v0.29.0...v0.30.0