You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Let a caller bring their own data dictionary, and keep it alive past the read. (#82)
#81 put the dictionary behind an interface. Nothing could supply one. A private
element's VR depends on the vendor who wrote the file, and the only way to teach
godicom a vendor's block was AddPrivateDictEntry, which mutates a process-global
table: every caller in the process sees the entry, and the only way to remove it
clears whatever anyone else registered too. Right for a program that owns its
process, wrong for a library.
ReadOptions.Dictionary is the per-read version, nil meaning PS3.6. It changes what
an element means rather than how it is described -- an implicit VR file carries no
VRs at all, so every element's VR is whatever the dictionary says, and for a
private element that answer depends on who wrote it. NewPrivateDictionary builds a
set of vendor entries that touches no global state, and NewDictionary composes:
each member is tried in turn and the first entry found wins, so the argument order
is the precedence. Compose rather than replace -- NewDictionary(vendor, Standard())
keeps the other 5,189 entries, and handing over the vendor's dictionary on its own
would leave every standard element as UN.
Three places the plumbing had to reach, none of them obvious from the signature
that needed changing:
The dictionary lives on readContext, not codecContext where the rest of the
per-element decode state lives, because it has to outlive the parse. A deferred
value is decoded on Get, long after the read returned, and deferred.go compares
the VR it re-resolves against the one the element was first read under. A
dictionary scoped to the parse would turn every deferred implicit-VR private
element into "deferred read VR UN does not match original US". readContext is the
only piece of parse state a Dataset retains, which is what makes it the right
home. Replacing rc.dictionary() with Standard() in datasetResolver reproduces that
error in both subtests of TestReadOptionsDictionarySurvivesADeferredLoad.
finishDeflated copies ReadOptions field by field, so a Deflated file would have
read its inflated dataset against PS3.6 while the same file undeflated resolved
properly. Only readReaderAt takes that path -- readBytes inflates in place and
keeps its readCtx -- so the test drives ReadFile as well as ReadBytes, and
dropping the copy fails it.
The VR-mismatch diagnostic is now judged against the dictionary the read was
given rather than always PS3.6. A caller who overrode an entry said what they
expect the file to contain; measuring against a different expectation than the
parse used would be reporting on nothing. Private tags stay exempt even when the
supplied dictionary has an entry, because the creator is not in hand at that point
and resolving one per element would charge the quiet path for a diagnostic nobody
asked for.
dictionaryVRForRead and lookupVRWithCreator are replaced by vrResolver, which
pairs the dictionary with the private-creator lookup because neither answers
alone -- a creator is only worth resolving against a dictionary that has that
vendor's block in it. Its zero value resolves against Standard with no creator,
which is what a header decoded outside a parse should get. LookupVR is now one
call to it.
AddPrivateDictEntry and ResetExtraPrivateDictionaries behave exactly as before and
are not deprecated; they are now implemented on top of PrivateDictionary rather
than a parallel table, and document the global mutation they perform.
Three gaps left deliberately, each recorded where someone would look for it:
- No WriteOptions.Dictionary. The write path resolves an ambiguous VR from the
dataset's own values -- Pixel Representation decides between US and SS -- and
never consults the dictionary, so the field would have nothing to do.
- DecodeDataset and friends take no options at all and so still resolve against
Standard(). Giving four functions an options parameter is a wider change than
the dictionary alone justifies.
- dictionaryHasTag stays a direct probe of the exact standard table. It runs
before the transfer syntax is known, to guess a byte order from the first tag;
a private block cannot help, and letting a supplied dictionary answer would
make the guess depend on how many entries the caller happened to add.
19 new tests in dictionary_composition_test.go, and ExampleNewPrivateDictionary
compile-checks the README snippet.
Closes#71.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
0 commit comments