godicom is a Go package for working with DICOM files. It lets you read, modify and write DICOM data with an idiomatic Go API.
godicom is a general-purpose DICOM framework concerned with reading and writing DICOM datasets. In order to keep the project manageable, it does not handle the specifics of individual SOP classes or DICOM networking. Other libraries in the godicom-dev organisation build on godicom for those areas — notably gonetdicom for DIMSE and DICOMweb (WADO-RS / QIDO-RS / STOW-RS).
go get github.com/godicom-dev/godicom@latestClone with the optional reference submodule (test fixtures):
git clone --recurse-submodules https://github.com/godicom-dev/godicom.gitThe pkg.go.dev API reference, CHANGELOG, and PARITY coverage map vs pydicom are the primary docs. Deferred items live in TODO.
Change a patient's ID
package main
import (
"log"
"github.com/godicom-dev/godicom"
"github.com/godicom-dev/godicom/tag"
)
func main() {
ds, err := godicom.ReadFile("ct.dcm", nil)
if err != nil {
log.Fatal(err)
}
if err := ds.SetString(tag.PatientID, "12345678"); err != nil {
log.Fatal(err)
}
if err := ds.SaveAs("ct_updated.dcm", nil); err != nil {
log.Fatal(err)
}
}Elements are accessed with typed getters and constants from the
tag package
(GetString, GetInt, GetFloat, GetBytes, GetSequence, …), and written
with the matching setters (SetString, SetInt, SetFloats, SetSequence, …),
which take the VR from the data dictionary and reject values the tag's VR cannot
hold. For private tags and other tags outside the dictionary, supply the VR
yourself with Set(godicom.NewDataElement(tag, vr, value)).
File I/O entry points: ReadFile / Read / ReadBytes / WriteFile /
FileDataset.SaveAs. Read accepts any io.Reader; prefer *os.File /
seekable sources so StopBeforePixels, DeferSize, SpecificTags, and
OnElement can skip large values without buffering them.
Reading only what you need
SpecificTags keeps the named tags at the top level of the dataset. A sequence
that is one of them is parsed whole — the filter does not walk into its items
and drop them. OnElement decides per element, including inside a sequence,
whether to parse the value, step over it, or stop:
want := map[godicom.Tag]bool{tag.PatientID: true}
ds, err := godicom.ReadBytes(data, &godicom.ReadOptions{
OnElement: func(h godicom.RawDataElement, path []godicom.PathStep) godicom.ElementAction {
if len(path) > 0 || want[h.Tag] {
return godicom.ElementKeep
}
return godicom.ElementSkip
},
})The hook is called after the header and before the value, so the
RawDataElement it receives has no value. On a seekable source ElementSkip
does not read the value bytes. Group 0x0002 and Specific Character Set are
never offered, because skipping either changes what the rest of the file means.
Context-aware variants (ReadFileContext, WriteContext,
DecodeDatasetContext, …) accept a context.Context for cancellation and
structured logging.
The data dictionary
godicom.Standard() returns the dictionary from PS3.6, and Lookup reads an
entry out of it:
entry, ok := godicom.Standard().Lookup(tag.PatientName, "")
// entry.VR "PN", entry.VM "1", entry.Name "Patient's Name",
// entry.Keyword "PatientName", entry.Retired falseThe second argument is the Private Creator, which a standard tag ignores and a
private tag needs — the same tag means different things to different vendors, so
without a creator there is no entry to find. Register a vendor's element with
AddPrivateDictEntry and Lookup finds it too.
A few entries permit more than one VR, which PS3.6 writes as prose. Pixel Data is one of them, so this is not a corner case:
entry, _ := godicom.Standard().Lookup(tag.PixelData, "")
// entry.VR "OB or OW", which is not a VR anything matches
// entry.VRs() []VR{"OB", "OW"}Your own dictionary
AddPrivateDictEntry mutates process-global state, which is right for a program
that owns its process and wrong for a library. NewPrivateDictionary builds one
that belongs to a single read instead, and ReadOptions.Dictionary is how it
gets there:
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()),
})NewDictionary composes: each dictionary is tried in turn and the first entry
found wins, so the order is the precedence. Compose rather than replace —
Standard() behind the vendor's dictionary is what keeps the other 5,189
entries, and handing over the vendor's on its own would leave every standard
element as UN.
This 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 it
is; the dataset that comes back retains the dictionary, because a deferred value
is decoded on Get, long after the read returned, and has to resolve to the VR
it was first read under.
There is 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 asks the dictionary, so the field would have nothing to do.
Private elements
A private element's tag is not fixed by the vendor's documentation. PS3.5 §7.8.1
makes the high byte of the element number a block the vendor reserves at run
time, by writing its name into (gggg,00xx) — the Private Creator. GE's
documented "element 1 of GEMS_ACQU_01" is therefore (0019,1001) in one file and
(0019,2001) in the next, depending on which block was free when each was
written. Code that hardcodes (0019,1001) reads a different vendor's data the
first time it meets a file where the blocks landed differently — and reads it
successfully, because there is nothing about the bytes to object to.
Name the vendor and let the dataset find the block:
block, ok := ds.PrivateBlock(0x0019, "GEMS_ACQU_01")
if !ok {
return // this file has nothing from that vendor in that group
}
elem, ok := block.Get(0x01) // (0019,1001) or (0019,2001), whichever this file usesNewPrivateBlock is the same lookup plus the ability to write, which is what a
dataset being built from nothing needs: with no Private Creator element there is
no block, and without a block there is nowhere to put a private element a reader
could ever attribute to you.
block, err := ds.NewPrivateBlock(0x0041, "ACME 3.2")
block.Set(0x01, godicom.VRUS, 4095) // also writes (0041,0010) "ACME 3.2", if absentReserving picks the lowest free block, so a second vendor in the same group lands
beside the first rather than on top of it. ds.PrivateCreators(0x0019) lists the
names that have reserved a block there, which is where to start with a file from
a manufacturer whose documentation you do not have: the tags say nothing, but the
creator names say who to ask.
Truncated and malformed files
By default a read keeps whatever it parsed before the file stopped making sense,
which is what most DICOM tooling does but hides the damage. Set
ReadOptions.OnDiagnostic to see those anomalies — a value shorter than its
length field, a header cut off mid-element, a sequence whose declared length
runs past the end of the file, a deferred value whose source has gone away, an
explicit VR the data dictionary cannot reconcile with its tag — each reported
with its tag, VR, byte offset, and the enclosing sequences down to the item it
came from, (0008,1140)[1] > (0008,1110)[1]:
ds, err := godicom.ReadFile("truncated.dcm", &godicom.ReadOptions{
OnDiagnostic: func(d godicom.Diagnostic) error {
log.Printf("%s", d) // keep parsing
return nil
},
})Diagnostic is itself an error, so returning it rejects the file instead:
opts := &godicom.ReadOptions{
OnDiagnostic: func(d godicom.Diagnostic) error { return d },
}A VR disagreement is the one anomaly that changes nothing about the parse: godicom keeps the VR the file gave it, because what the file says is what the file means. It is reported because it is the first thing worth knowing when a device refuses your files.
Character sets godicom cannot decode
An unknown or private Specific Character Set still falls back to ISO_IR 6 —
guessing a national encoding would turn a detectable error into an undetectable
one — but it is no longer silent. The reader reports
DiagnosticUnknownCharacterSet once for the (0008,0005) element, carrying the
original spelling and index of every unresolved value; an invalid byte sequence
in a text VR reports DiagnosticInvalidCharacterData. A PACS archiving endpoint
that must never store mojibake can refuse on the hook:
opts := &godicom.ReadOptions{
OnDiagnostic: func(d godicom.Diagnostic) error {
if d.Kind == godicom.DiagnosticUnknownCharacterSet ||
d.Kind == godicom.DiagnosticInvalidCharacterData {
return d // refuse the file; quarantine the original bytes instead
}
return nil
},
})Without a hook nothing about the parse changes, so tools that browse historical
dirty data keep working. Legal CS padding such as "GB18030 \ISO_IR 100" is
normalized and never reported.
DiagnosticInvalidCharacterData is reported element by element only up to a
point: one dirty study fails thousands of values, and a library that hands a hook
a struct and a log line per value wastes the caller's memory to say the same
thing repeatedly. The reader reports the first eight
(DefaultMaxInvalidCharacterDataDiagnostics) and then, when the dataset
finishes, one summary whose Count says how many that summary stands for and
whose tag, offset and path point at the first of them. ReadOptions. MaxInvalidCharacterDataDiagnostics moves the cap; a negative value removes it,
which is what a caller building a per-element report wants.
Both diagnostics describe a declaration godicom can see is wrong. They do not
catch a file that declares ISO_IR 6 — or omits (0008,0005) — and ships
national bytes anyway. That declaration is legal, and godicom keeps the
repertoire the file asked for instead of guessing, so such a value still comes
back undecoded. Detecting it means validating every text value against
ISO_IR 6, which would also flag Latin-1 free text that has been archived
unremarked for twenty years, so it is opt-in:
opts := &godicom.ReadOptions{
ReportNonASCIIUnderDefaultCharset: true,
OnDiagnostic: func(d godicom.Diagnostic) error {
if d.Kind == godicom.DiagnosticMisdeclaredCharacterSet {
// d.Tag names the first element that proves the declaration wrong
return d // or quarantine the study and re-read it with the right set
}
return nil
},
}It fires once per dataset, not once per element: the wrong declaration is one
fact about the file. Even then the reader does not guess — it only tells you the
bytes cannot mean what the header says, and leaves the repair to
ReinterpretCharacterSet.
When you know the declaration is wrong, say what it should have been instead of
editing (0008,0005) and saving: the value holds the bytes of the file, not
text, so the writer refuses that change with ErrCharsetChangeLosesText rather
than re-encoding the bytes into replacement characters.
fd, err := godicom.ReadFile("misdeclared.dcm", nil)
if err != nil {
log.Fatal(err)
}
if _, err := fd.ReinterpretCharacterSet("GB18030"); err != nil {
log.Fatal(err) // undecodable bytes: quarantine the file instead
}
// PatientName is the name now, (0008,0005) says GB18030, and the bytes on disk
// do not change.Values a strict receiver would reject
WriteOptions.OnDiagnostic is the same hook on the way out. It reports values
the writer would otherwise encode silently even though godicom's own reader
raises a diagnostic on the result — an IS outside [-2^31, 2^31), a DS
longer than the 16 bytes PS3.5 allows, a fractional value in an IS:
if err := ds.SetInt(tag.EchoNumbers, 3000000000); err != nil {
log.Fatal(err)
}
err := godicom.WriteFile("out.dcm", ds, &godicom.WriteOptions{
OnDiagnostic: func(d godicom.Diagnostic) error { return d },
})
// err: godicom: error writing dataset: godicom: invalid_value at (0018,0086)
// IS: "3000000000" is outside [-2147483648, 2147483647], the range an IS allowsReturning nil writes the value as it stands, so nothing an existing caller
writes changes; returning the diagnostic fails the write. That 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 you want.
The dictionary-VR setters reject at the call site whatever they can see on their
own: SetFloat(tag.EchoNumbers, 1.5) fails immediately, because an IS holds
no floating-point values. The hook is for what a setter cannot judge — a value
in range for its Go type but not for its VR, or an element built directly with
Set(NewDataElement(tag, vr, value)).
Dataset bytes (no File Meta)
Encode or decode a dataset without a Part 10 preamble — useful for DIMSE or multipart payloads:
data, err := ds.Encode(uid.ExplicitVRLittleEndian)
parsed, err := godicom.DecodeDataset(data, uid.ExplicitVRLittleEndian)Part 10 files in memory:
bytes, err := ds.EncodeFile(nil)
ds2, err := godicom.ReadBytes(bytes, nil)Compressed and uncompressed Pixel Data can be read as raw bytes or decoded frames:
import "github.com/godicom-dev/godicom/pixels"
ds, err := godicom.ReadFile("mr_j2k.dcm", nil)
if err != nil {
log.Fatal(err)
}
// All frames concatenated (native layout)
raw, err := ds.PixelBytes(pixels.WithRaw(true))
// Or one frame at a time
frames, err := ds.PixelFrames(pixels.WithRaw(true), pixels.WithFrameIndex(0))With WithRaw(false) (the default), decoded frames are normalised for display
(for example YBR→RGB and planar configuration). Modality / VOI LUT helpers are
available separately and are not applied automatically by PixelBytes.
For pydicom-style access, use PixelArray (decoded samples + shape) and
DisplayFrame (8-bit display-ready bytes after modality / VOI / presentation).
A PALETTE COLOR image is expanded through its color lookup table instead of
that grayscale pipeline. ApplyColorLUT returns the full-depth samples, and
can take one of the eight well-known palettes by name or SOP Instance UID:
arr, err := ds.PixelArray(pixels.WithRaw(true))
frame, err := ds.DisplayFrame(0)
samples, err := ds.PixelSamples(pixels.WithRaw(true))
rgb, err := godicom.ApplyColorLUT(samples, ds, "")
rgb, err = godicom.ApplyColorLUT(samples, nil, "PET")| Format | Package |
|---|---|
| JPEG / JPEG-LS | golibjpeg |
| JPEG 2000 / HTJ2K | goopenjpeg |
| RLE Lossless | gorle |
These are pulled in automatically as module dependencies. Native (uncompressed) and Deflated transfer syntaxes need no extra plugins.
err := ds.CompressPixelData(uid.RLELossless)
err = ds.CompressPixelData(uid.JPEGLSLossless)
err = ds.CompressPixelData(uid.JPEG2000Lossless)
err = ds.CompressPixelData(uid.JPEG2000) // lossy JPEG 2000Supported encode paths: native, RLE Lossless, Deflated, JPEG (baseline /
lossless / JPEG-LS), JPEG 2000 (lossless / lossy), and HTJ2K (.201 lossless
LRCP, .202 lossless RPCL, .203 lossy).
OpenFileSet reads a DICOMDIR and walks the directory records in the order
their offsets link them. Each instance's path is the DICOMDIR's directory plus
its Referenced File ID.
fs, err := godicom.OpenFileSet("DICOMDIR", nil)
for _, inst := range fs.Instances() {
fmt.Println(inst.Path())
}
matches := fs.Find(map[godicom.Tag]string{
tag.PatientID: "77654033",
})
ds, err := matches[0].Load()tag is github.com/godicom-dev/godicom/tag.
To write the DICOMDIR back, WriteDICOMDIR writes a new one at the path given
and Write replaces the File-set's own; both rebuild the offsets from the tree,
so a record added or dropped before writing is linked correctly.
err = fs.WriteDICOMDIR("out/DICOMDIR", nil)WriteDICOMDIROptions{ForceImplicitVR: true} writes implicit VR little endian,
which PS3.10 forbids for a DICOMDIR and some older readers require.
NewFileSet builds one from instances instead:
fs, err := godicom.NewFileSet()
fs.ID = "STUDY42"
_, err = fs.AddFile("img/ct1.dcm")
_, err = fs.AddFile("img/ct2.dcm")
err = fs.Save("out/fileset", nil)Add files an instance under the records its Patient, Study and Series UIDs
put it in, sharing the PATIENT, STUDY and SERIES records the File-set already
has, and Save writes each file at the File ID its place implies
(PT000000/ST000000/SE000000/IM000000) and then the DICOMDIR. Remove drops an
instance from the tree but leaves its file on disk, so a removal can be undone.
Save with FileSetWriteOptions{UseExisting: true} keeps the layout a File-set
was read with and rewrites only the DICOMDIR.
A PRIVATE record carries nothing that says where it belongs, so Add cannot
build one; AddCustom takes the records and fills in only the elements that
link them to the file:
record := godicom.NewDataset() // the PATIENT record
record.Set(godicom.NewDataElement(tag.DirectoryRecordType, godicom.VRCS, "PATIENT"))
record.Set(godicom.NewDataElement(tag.PatientID, godicom.VRLO, "77654033"))
top := &godicom.RecordNode{Record: record}
// ... the PRIVATE record, hung under top, becomes the leaf ...
_, err = fs.AddCustom(ds, leaf)CopyTo copies a File-set to another root and returns the File-set that lives
there. The original keeps its layout, so it can still be saved:
copied, err := fs.CopyTo("out/copy", nil)A DICOMDIR whose offset links never reach some records still holds everything
those records named: the reader reads each instance and rebuilds the records it
calls for, which is what pydicom does. FileSetOptions{ExcludeOrphans: true}
leaves them out, and RaiseOrphans reports them.
OverlayArray unpacks a 1-bit Overlay Plane. group is the repeating group,
0x6000 for the first plane:
ov, err := ds.OverlayArray(0x6000)
bit := ov.At(0, row, col) // 0 or 1godicom uses Go's log/slog. By default it is silent (DiscardHandler),
similar in spirit to leaving pydicom's debugger off until you call
config.debug().
import (
"context"
"log/slog"
"os"
"github.com/godicom-dev/godicom"
)
h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})
logger := slog.New(h)
// Per-call (preferred)
ds, err := godicom.ReadFile("ct.dcm", &godicom.ReadOptions{Logger: logger})
// Or via context (request-scoped / shared with gonetdicom)
ctx := godicom.WithLogger(context.Background(), logger)
ds, err = godicom.ReadFileContext(ctx, "ct.dcm", nil)CLI: godicom show -debug file.dcm
Debug records use fixed attribute keys aligned with pydicom filereader
diagnostics (component, offset, offset_hex, hex, tag, vr, len,
undefined_length, value_hex, value, transfer_syntax, …). Messages cover
the same events as pydicom's debugger: FMI/DICM, per-element header + value
preview (first 20 bytes), defer skips, and sequence item boundaries.
import "github.com/godicom-dev/godicom/dicomjson"
jsonData, err := dicomjson.MarshalDataset(ds.Dataset)
parsed, err := dicomjson.ParseDataset(jsonData)
arr, err := dicomjson.MarshalDatasets([]*godicom.Dataset{ds1, ds2})
dss, err := dicomjson.ParseDatasets(arr)sr holds PS3.16: the codes, and the contexts (CIDs) that group them. It is
pydicom's pydicom.sr, which since 3.0 is the code dictionary and nothing else.
import "github.com/godicom-dev/godicom/sr"
lung, err := sr.Scheme("SCT")
code, err := lung.Lookup("Lung") // (SCT, "39607008", "Lung")
context, err := sr.CID(2) // the contexts PS3.16 defines
transverse, err := context.Lookup("Transverse")
names := context.Dir("Thoracic") // the context's words, filteredA keyword is the camel case form of a code meaning, as in pydicom, so
codes.SCT.Lung is sr.Scheme("SCT").Lookup("Lung"). Code.Equal compares the
concept and not the text: the meaning does not take part, and a SNOMED code
given under the retired SRT designator equals the same code under SCT. That
is a method rather than == because Go's comparison cannot do it, which also
means a Code is not a map key.
codes := sr.Codes() // every collection, loaded once
codes.Schemes() // ["DCM", "SCT", ...]
codes.CIDs() // ["CID2", "CID4", ...]
anatomic, err := sr.CID(244)
if anatomic.ContainsCode(code) { ... } // the concept, not the textThe terminology is 2.4 MB of text in sr/codes.txt, generated from pydicom's
three dictionaries by generate_sr_dict.py and read on first use. It is all of
PS3.16: 18 schemes, 18735 codes, 1355 contexts and 27033 memberships.
waveforms reads the Waveform module of PS3.3 C.10.9: the multiplex groups of
a (5400,0100) Waveform Sequence as arrays of samples. It is pydicom's
pydicom.waveforms, which is one handler over numpy; this one is pure Go.
import "github.com/godicom-dev/godicom/waveforms"
groups, err := waveforms.Multiplexes(ds.Dataset)
m := groups[0] // 10000 samples of 12 channels
v := m.At(0, 0) // as float64
r := m.RawAt(0, 0) // the raw sample, exactly
convert, err := m.WithChannelSensitivity() // the Channel Sensitivity of
v = convert.At(0, 0) // every definition appliedRawAt returns the sample as the integer it is, exactly, for all ten
Bits Allocated / Sample Interpretation pairs the module defines;
WithChannelSensitivity converts by each channel definition's Channel
Sensitivity, Correction Factor and Baseline, which is pydicom's as_raw=False.
The Waveform Data is read little endian, which is what pydicom's hardcoded
dtypes do even for a big endian file.
go install github.com/godicom-dev/godicom/cmd/godicom@latest
godicom show <file> # print file meta + dataset
godicom read <file> # alias for show
godicom readcopy <src> <dst> # read, write, re-readshow and read take these flags, in any combination:
| Flag | Effect |
|---|---|
-no-meta |
Skip the file meta information group, print the dataset only |
-top |
Restrict the -t search to top-level elements instead of recursing into sequences. No effect on its own: plain show never descends into a sequence anyway |
-debug |
Emit reader debug logs to stderr |
-t <tag> |
Print only this tag. A keyword like PatientName, bare hex like 00080060, or parenthesised hex like "(0008,0060)" — the comma form needs the parentheses. Repeatable |
-tag <tag> |
Alias for -t |
godicom show -no-meta <file> # dataset only, no file meta group
godicom show -t PatientName -t 00080060 <file> # two tags, by keyword and by hex
godicom show -t TreatmentMachineName <file> # found inside BeamSequence
godicom show -t TreatmentMachineName -top <file> # not found: -top skips sequences| Transfer Syntax | Read | Write |
|---|---|---|
| Explicit / Implicit VR Little Endian | ✅ | ✅ |
| Explicit VR Big Endian | ✅ | ✅ |
| Deflated Explicit VR Little Endian | ✅ | ✅ |
| RLE Lossless | ✅ | ✅ |
| JPEG Baseline / Extended / Lossless | ✅ | ✅ |
| JPEG-LS | ✅ | ✅ |
| JPEG 2000 / HTJ2K | ✅ | ✅ |
JPEG, JPEG-LS, JPEG 2000 and HTJ2K are decoded and encoded by golibjpeg and goopenjpeg. Everything else in the table — including RLE and Deflated — is pure Go with no native code involved.
godicom builds and runs anywhere Go does. The native codecs ship prebuilt libraries for six platforms:
| Platform | JPEG / JPEG-LS / JPEG 2000 / HTJ2K |
|---|---|
| linux/amd64, linux/arm64 | ✅ |
| darwin/amd64, darwin/arm64 | ✅ |
| windows/amd64, windows/arm64 | ✅ |
| everything else | reading and writing work; compressed pixel data returns an error |
There is no cgo and no toolchain to install: the libraries are loaded through
purego, so a plain go build is all a
cross-compile takes.
On a platform without a prebuilt library, importing godicom is still safe. Parsing, writing, the data dictionary, JSON, RLE and Deflated all work; only the four native-codec transfer syntaxes fail, and they fail with an error rather than a panic:
if _, err := ds.PixelBytes(); errors.Is(err, golibjpeg.ErrUnsupportedPlatform) {
// No JPEG library for this GOOS/GOARCH. The dataset itself is fine.
}CI builds every release for 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 32-bit. It also runs the full suite on
linux/arm64, because that is one of the six platforms whose native libraries
are embedded and the only one a cross-compile cannot vouch for: they are loaded
at run time, so a build says nothing about whether they load. linux/mips and
linux/mipsle do not build, because purego does not support them yet.
A binary carries one platform's libraries, never all six. The //go:embed
directives are behind per-platform build tags, so the linker only ever sees the
pair for the target you are building:
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 together are 17.0 MB, so embedding them unconditionally would add about 13.3 MB to every binary — a linux/amd64 build would be 24.3 MB instead of 11.0 MB. To confirm what your own build embeds:
go list -f '{{.EmbedFiles}}' github.com/godicom-dev/golibjpeg/nativeBug reports, fixes, and documentation improvements are welcome. Please open an issue or pull request on GitHub.
MIT — see LICENSE.