Skip to content

Releases: godicom-dev/godicom

v0.29.0

Choose a tag to compare

@xbmlz xbmlz released this 25 Aug 03:07
383f72d

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/native

There 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,
    which pixels.EncodeFrame has dispatched on for some time.
  • The README documents platform support and binary size, and its Go snippets are
    now Example functions that go test compiles and checks.

Full changelog: https://github.com/godicom-dev/godicom/blob/v0.29.0/CHANGELOG.md

v0.28.0

Choose a tag to compare

@xbmlz xbmlz released this 24 Aug 03:28
e787c3d

Changed

  • BREAKING: Diagnostic.Path is now []PathStep instead 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 reads in (0008,1140)[1] > (0008,1110)[1] in Diagnostic.Error() and in the sequence_path log attribute. Item is -1, and the subscript is dropped, when the sequence was entered but no item was: the item header itself was unreadable. Callers comparing d.Path[0] to a Tag compare it to PathStep{Tag: t, Item: i} instead, or read d.Path[0].Tag
  • Internal: the read and write chains carry the encoding triple (isImplicitVR, isLittleEndian, charsets) as one codecContext instead 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 as SH when the dictionary says PN, or a known tag sent as UN. The new DiagnosticVRMismatch kind carries the encoded VR in Diagnostic.VR and the dictionary's in the new Diagnostic.ExpectedVR field. 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, so PixelData does not report on every image. The check costs a dictionary lookup per element, so it is skipped unless an OnDiagnostic hook is set or warn-level logging is on

  • Write diagnostics: WriteOptions.OnDiagnostic func(Diagnostic) error mirrors ReadOptions.OnDiagnostic and reports values the writer would otherwise encode silently even though godicom's own reader raises a diagnostic on the result — a fractional float64 in an IS (1.5 is not an integer string), a DS longer than the 16 bytes PS3.5 allows, an IS outside [-2^31, 2^31). The new DiagnosticInvalidValue kind identifies them; they carry no Offset, since nothing was read. Returning nil keeps 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 / RAISE in config.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 DS element holding a plain float64 — what SetFloat / SetFloats store, since DS is a float VR — was written with an unbounded %g while PS3.5 caps DS at 16 bytes. SetFloat(SliceThickness, 1.0/3.0) wrote "0.3333333333333333" (18 bytes), which godicom's own IsValidDS rejects and a strict receiver may refuse. The writer now applies the same FormatNumberAsDS truncation the DS type and pydicom's format_number_as_ds use, so a value stored as a float64 reaches the file identically to the same value stored as a DS. A DS parsed from a file still round-trips its original string byte for byte, over-long or not
  • NaN and the infinities were written into a DS as the literal bytes "NaN" / "+Inf" / "-Inf" with no error reported anywhere, and godicom's tolerant ParseDS read them back — a decimal string has no spelling for any of them. SetFloat / SetFloats now reject them for a DS tag at the call site, and the writer refuses them rather than emitting an invalid DS or IS. FD and FL are unaffected: they represent all three exactly, per IEEE 754

v0.27.0

Choose a tag to compare

@xbmlz xbmlz released this 20 Aug 06:30
d7ab30b

Added

  • Diagnostics: ReadOptions.OnDiagnostic reports 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. Diagnostic is itself an error, so returning it from the hook rejects the file while returning nil keeps the tolerant default
  • Setters: dictionary-VR setters SetString / SetInt / SetFloat / SetBytes / SetSequence, the plural SetStrings / SetInts / SetFloats, and SetDA / 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 need Set(NewDataElement(tag, vr, value))

Changed

  • BREAKING: transfer syntax parameters and returns are uid.UID, not string: EncodeDataset, WriteDataset, Dataset.Encode, DecodeDataset, DecodeDatasetContext, CompressPixelData, CompressPixelDataContext, FileDataset.TransferSyntaxUID, and pixels.FileSource. String literals still compile at call sites; string variables need an explicit uid.UID(...)
  • Read hands a reader that already offers io.ReaderAt plus Size() 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 Read from a seekable reader that is not an *os.File (bytes.Reader, io.SectionReader, range-request adapters): the tag stayed listed while Get / GetBytes reported it absent
  • Deferred values in a Deflated dataset read through ReadBytes / Read loaded from the still-compressed buffer, failing with a tag or VR mismatch. ReadFile was 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 ReadBytes and pydicom's read_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
  • AT elements 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 wrote AT values that it and every other implementation read back swapped: (0018,1063) came back as (1063,0018). encodeAT also accepts []Tag, []int and *MultiValue[int] now, so SetInts on an AT tag encodes rather than silently producing an empty value

v0.26.0

Choose a tag to compare

@xbmlz xbmlz released this 12 Aug 16:26
b8ed3c9

Added

  • Logging: quiet-by-default log/slog foundation (WithLogger / LoggerFromContext / SetDefaultLogger); ReadOptions.Logger / WriteOptions.Logger; *Context entry points; CLI godicom show -debug
  • Pixels: PixelArray and DisplayFrame (pydicom-style pixel access + 8-bit display pipeline)
  • Pixels: HTJ2K encode for transfer syntaxes .201 / .202 / .203 via goopenjpeg v1.2.0

Changed

  • Docs: README aligned with pydicom-style layout

v0.25.1

Choose a tag to compare

@xbmlz xbmlz released this 07 Aug 03:21
b3f7b87

Fixed

  • CI: bump golibjpeg to v1.2.1 — Linux/Windows prebuilt libs now include golibjpeg_encode (v1.2.0 regression)
  • gofmt on pixels/encode.go

Use v0.25.1 instead of v0.25.0 on Linux/Windows.

v0.25.0

Choose a tag to compare

@xbmlz xbmlz released this 07 Aug 03:09
69de2cb

Added

  • Pixels: JPEG baseline / extended / lossless and JPEG-LS encode via golibjpeg v1.2.0 (EncodeFrame, CompressPixelData)

Fixed

  • Read: ambiguous VR elements keep raw []byte at read time so Implicit VR LE PixelData works with GetBytes / PixelBytes (#45)

Changed

  • golibjpeg dependency: v1.1.2 → v1.2.0

Tests: 731 passed

v0.24.0

Choose a tag to compare

@xbmlz xbmlz released this 18 Jul 11:55
75268fa

Added

  • uid: GenerateUID / MustGenerateUID (WithPrefix, WithUUIDPrefix, WithEntropy)
  • Read: Read(io.Reader) — seekable sources parse without ReadAll; StopBeforePixels / DeferSize / SpecificTags skip large values

Changed

  • ReadFile uses the seekable streaming path
  • Docs: English README/TODO; PARITY.md coverage map

v0.20.0

Choose a tag to compare

@xbmlz xbmlz released this 13 Jul 08:56
d7ce859

What's Changed

  • feat: v0.20 JPEG 2000 encode via goopenjpeg by @xbmlz in #39

Full Changelog: v0.19.0...v0.20.0

v0.19.0

Choose a tag to compare

@xbmlz xbmlz released this 13 Jul 08:26
1dac6df

What's Changed

  • feat: v0.19 pixel encode (RLE / deflated / encapsulate) by @xbmlz in #38

Full Changelog: v0.18.0...v0.19.0

v0.18.0

Choose a tag to compare

@xbmlz xbmlz released this 13 Jul 08:01
136c2b9

What's Changed

  • feat: v0.18 modality rescale and VOI windowing by @xbmlz in #37

Full Changelog: v0.17.0...v0.18.0