Releases: godicom-dev/godicom
Release list
v0.29.0
Six platforms per codec instead of four, 32-bit support, and three real bugs that
were only visible once a 32-bit build existed.
The codecs cover more platforms
golibjpeg and goopenjpeg are now v1.3.0, adding prebuilt libraries for
darwin/amd64 and windows/arm64:
| Platform | JPEG / JPEG-LS / JPEG 2000 / HTJ2K |
|---|---|
| linux/amd64, linux/arm64 | ✅ |
| darwin/amd64 | ✅ new |
| darwin/arm64 | ✅ |
| windows/amd64 | ✅ |
| windows/arm64 | ✅ new |
More importantly, both now build everywhere Go targets rather than only where
they ship a library, and return an error wrapping ErrUnsupportedPlatform from
every entry point instead of panicking. Importing godicom is therefore safe on
any platform: parsing, writing, the data dictionary, JSON, RLE and Deflated all
work, and only the four native-codec transfer syntaxes fail — with an error you
can test for.
if _, err := ds.PixelBytes(); errors.Is(err, golibjpeg.ErrUnsupportedPlatform) {
// No JPEG library for this GOOS/GOARCH. The dataset itself is fine.
}Three bugs
Checking that "importable anywhere" claim turned out to falsify it — godicom
itself did not compile for a 32-bit target. Fixing that surfaced two bugs with
nothing to do with cross-compilation.
The data dictionary answered US for nearly every tag on a 32-bit platform.
All 88 repeater masks were parsed with fmt.Sscanf into int fields with the
error discarded. A mask like 0xFF00FFFF does not fit in a 32-bit int, so all
88 came out zero — and (tag ^ value) & 0 == 0 is true for every tag.
Whichever repeater came first in map iteration order answered for every
non-private tag: LookupVR(PatientName) returned US rather than PN, group
lengths returned US rather than UL, IsRepeaterTag was true for everything,
and reading an ordinary file raised two dozen spurious VR-mismatch diagnostics.
64-bit platforms were unaffected.
ParseTag rejected every hex tag with a group of 0x8000 or above — on all
platforms. It used strconv.ParseInt(v, 16, 32), and a signed 32-bit parse
rejects anything from 0x80000000 up:
ParseTag("FFFEE000") // godicom: unknown tag keyword "FFFEE000"Those are the item, item-delimiter and sequence-delimiter tags that godicom's own
sequence and encapsulation code is built from, and the ones a JSONKey
round-trip is most likely to hit. pydicom's int(arg, 16) has no width at all,
so ParseUint is the port. The parenthesised form (FFFE,E000) always worked.
encaps.Encapsulate did not compile on 32-bit. Its Basic Offset Table guard
read total > (1<<32)-1, exact in pydicom because Python integers are
arbitrary-precision, an int overflow at compile time in Go.
Lengths are uint32 now
The compile failures were one mistake repeated: a declared value length is
unsigned 32 bits, and elementHeader.Length was an int. 0xFFFFFFFF —
undefined length — is -1 in an int on a 32-bit platform, so casting to make
it build would have shipped a binary that silently misread every
undefined-length sequence and every encapsulated Pixel Data element. A
merely-compiling build would have been worse than the compile error.
uint32 is carried through the header, deferred-read and sequence paths
instead. No exported signature changed, and it removed two existing
uint32(length) casts at call sites.
One platform's libraries per binary
Each binary embeds the libraries for its own platform and never all twelve. The
//go:embed directives sit behind per-platform build tags:
cmd/godicom, go build |
Size | Embedded libraries |
|---|---|---|
| linux/amd64 | 11.0 MB | 3.7 MB |
| linux/arm64 | 10.4 MB | 3.4 MB |
| darwin/amd64 | 10.4 MB | 2.7 MB |
| darwin/arm64 | 9.8 MB | 2.3 MB |
| windows/amd64 | 10.5 MB | 2.6 MB |
| windows/arm64 | 9.8 MB | 2.4 MB |
| js/wasm | 8.3 MB | none |
| linux/386 | 6.4 MB | none |
All twelve libraries are 17.0 MB together, so embedding them unconditionally
would add about 13.3 MB to every binary — linux/amd64 would be 24.3 MB instead
of 11.0 MB. To check your own build:
go list -f '{{.EmbedFiles}}' github.com/godicom-dev/golibjpeg/nativeThere is no cgo and no toolchain to install; the libraries load through purego,
so a plain go build is all a cross-compile takes.
CI keeps it true
The new cross-build job vets windows/386, linux/386, linux/arm (including
GOARM=5), linux/riscv64, linux/ppc64le, js/wasm and wasip1/wasm, and
runs the full test suite on linux/386. The test run is the point: the
repeater-mask bug compiled cleanly and only failed when executed. linux/mips
and linux/mipsle are excluded because purego does not build for them yet.
Tests that genuinely need a native codec now skip rather than fail where no
library exists, so a real failure is visible among them.
Also
CompressPixelData's doc comment listed its supported targets without HTJ2K,
whichpixels.EncodeFramehas dispatched on for some time.- The README documents platform support and binary size, and its Go snippets are
nowExamplefunctions thatgo testcompiles and checks.
Full changelog: https://github.com/godicom-dev/godicom/blob/v0.29.0/CHANGELOG.md
v0.28.0
Changed
- BREAKING:
Diagnostic.Pathis now[]PathStepinstead of[]Tag, so it names which item of each enclosing sequence the anomaly came from and not merely which sequence. PS3.5 gives sequence items an ordinal position and nothing else to identify them by, so a forty-item sequence used to produce forty diagnostics that read identically.PathStep{Tag, Item}renders the way DICOM tooling spells it —(0040,0100)[0]— and a whole path readsin (0008,1140)[1] > (0008,1110)[1]inDiagnostic.Error()and in thesequence_pathlog attribute.Itemis -1, and the subscript is dropped, when the sequence was entered but no item was: the item header itself was unreadable. Callers comparingd.Path[0]to aTagcompare it toPathStep{Tag: t, Item: i}instead, or readd.Path[0].Tag - Internal: the read and write chains carry the encoding triple (
isImplicitVR,isLittleEndian,charsets) as onecodecContextinstead of three parameters threaded through fourteen signatures. The two bools are adjacent and interchangeable, so a transposed call site used to compile and silently encode or parse a file in the wrong byte order
Added
-
VR disagreement diagnostics: reading now reports an explicit VR the data dictionary cannot reconcile with its tag — a
(0010,0010)encoded asSHwhen the dictionary saysPN, or a known tag sent asUN. The newDiagnosticVRMismatchkind carries the encoded VR inDiagnostic.VRand the dictionary's in the newDiagnostic.ExpectedVRfield. The parse is unchanged — godicom still keeps the VR the file gave it, because what the file says is what the file means — so this is pure information about interoperability, and returning the diagnostic from the hook turns it into a read failure. Private tags, tags absent from the dictionary, and implicit VR are excluded: they have no dictionary VR to fall short of. A dictionary entry naming more than one permitted VR ("OB or OW","US or SS") is satisfied by any of them, soPixelDatadoes not report on every image. The check costs a dictionary lookup per element, so it is skipped unless anOnDiagnostichook is set or warn-level logging is on -
Write diagnostics:
WriteOptions.OnDiagnostic func(Diagnostic) errormirrorsReadOptions.OnDiagnosticand reports values the writer would otherwise encode silently even though godicom's own reader raises a diagnostic on the result — a fractionalfloat64in anIS(1.5is not an integer string), aDSlonger than the 16 bytes PS3.5 allows, anISoutside[-2^31, 2^31). The newDiagnosticInvalidValuekind identifies them; they carry noOffset, since nothing was read. Returningnilkeeps the old behaviour and writes the value as it stands, so no existing caller changes; returning the diagnostic fails the write:opts := &WriteOptions{OnDiagnostic: func(d Diagnostic) error { return d }}
This is the three-way choice pydicom spells
IGNORE/WARN/RAISEinconfig.settings.writing_validation_mode, without a mode enum: whether the hook is set, and what it returns, says which one the caller wants. Values written back from the bytes they were read as are not offered — they are not re-encoded, and the read had its own chance to report them
Fixed
- A
DSelement holding a plainfloat64— whatSetFloat/SetFloatsstore, sinceDSis a float VR — was written with an unbounded%gwhile PS3.5 capsDSat 16 bytes.SetFloat(SliceThickness, 1.0/3.0)wrote"0.3333333333333333"(18 bytes), which godicom's ownIsValidDSrejects and a strict receiver may refuse. The writer now applies the sameFormatNumberAsDStruncation theDStype and pydicom'sformat_number_as_dsuse, so a value stored as afloat64reaches the file identically to the same value stored as aDS. ADSparsed from a file still round-trips its original string byte for byte, over-long or not NaNand the infinities were written into aDSas the literal bytes"NaN"/"+Inf"/"-Inf"with no error reported anywhere, and godicom's tolerantParseDSread them back — a decimal string has no spelling for any of them.SetFloat/SetFloatsnow reject them for aDStag at the call site, and the writer refuses them rather than emitting an invalidDSorIS.FDandFLare unaffected: they represent all three exactly, per IEEE 754
v0.27.0
Added
- Diagnostics:
ReadOptions.OnDiagnosticreports parse anomalies — a value shorter than its length field, a header cut off mid-element, a sequence item header past the end of the file, a deferred value whose source has gone away — each carrying its tag, VR, byte offset, and enclosing sequences.Diagnosticis itself anerror, so returning it from the hook rejects the file while returningnilkeeps the tolerant default - Setters: dictionary-VR setters
SetString/SetInt/SetFloat/SetBytes/SetSequence, the pluralSetStrings/SetInts/SetFloats, andSetDA/SetTM/SetDT/SetPN/SetDS/SetIS. Each resolves the VR from the data dictionary and rejects value kinds that VR cannot hold. Tags outside the dictionary still needSet(NewDataElement(tag, vr, value))
Changed
- BREAKING: transfer syntax parameters and returns are
uid.UID, notstring:EncodeDataset,WriteDataset,Dataset.Encode,DecodeDataset,DecodeDatasetContext,CompressPixelData,CompressPixelDataContext,FileDataset.TransferSyntaxUID, andpixels.FileSource. String literals still compile at call sites;stringvariables need an explicituid.UID(...) Readhands a reader that already offersio.ReaderAtplusSize() int64(*bytes.Reader,*strings.Reader,*io.SectionReader) to the parser as-is instead of wrapping it in Seek+Read, so neither the parse nor a later deferred load moves the caller's offset- Internal: one shared data element header decoder for the streaming and in-memory readers; the sequence readers return errors instead of latching them on the read context
Fixed
- Deferred values were permanently unreachable after
Readfrom a seekable reader that is not an*os.File(bytes.Reader,io.SectionReader, range-request adapters): the tag stayed listed whileGet/GetBytesreported it absent - Deferred values in a Deflated dataset read through
ReadBytes/Readloaded from the still-compressed buffer, failing with a tag or VR mismatch.ReadFilewas unaffected - The streaming reader silently dropped a defined-length sequence whose length ran past the end of the file, complete items included. It now parses the items that are present and reports a truncated item, matching
ReadBytesand pydicom'sread_sequence_item - A diagnostic raised inside a sequence — and a hook error rejecting it — now reaches the caller instead of being swallowed by the sequence readers
ATelements were encoded as a single 32-bit value instead of the group / element pair of 16-bit values required by PS3.5 7.1.1. The two coincide under big endian and differ under little endian, so godicom wroteATvalues that it and every other implementation read back swapped:(0018,1063)came back as(1063,0018).encodeATalso accepts[]Tag,[]intand*MultiValue[int]now, soSetIntson anATtag encodes rather than silently producing an empty value
v0.26.0
Added
- Logging: quiet-by-default
log/slogfoundation (WithLogger/LoggerFromContext/SetDefaultLogger);ReadOptions.Logger/WriteOptions.Logger;*Contextentry points; CLIgodicom show -debug - Pixels:
PixelArrayandDisplayFrame(pydicom-style pixel access + 8-bit display pipeline) - Pixels: HTJ2K encode for transfer syntaxes
.201/.202/.203via goopenjpeg v1.2.0
Changed
- Docs: README aligned with pydicom-style layout
v0.25.1
v0.25.0
Added
- Pixels: JPEG baseline / extended / lossless and JPEG-LS encode via
golibjpegv1.2.0 (EncodeFrame,CompressPixelData)
Fixed
- Read: ambiguous VR elements keep raw
[]byteat read time so Implicit VR LEPixelDataworks withGetBytes/PixelBytes(#45)
Changed
- golibjpeg dependency: v1.1.2 → v1.2.0
Tests: 731 passed
v0.24.0
Added
- uid:
GenerateUID/MustGenerateUID(WithPrefix,WithUUIDPrefix,WithEntropy) - Read:
Read(io.Reader)— seekable sources parse withoutReadAll;StopBeforePixels/DeferSize/SpecificTagsskip large values
Changed
- ReadFile uses the seekable streaming path
- Docs: English README/TODO; PARITY.md coverage map