Skip to content
godicom-devPublic

Latest commit

 

History

250 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CI Coverage Go Version GoDoc License: MIT

godicom

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).

Installation

go get github.com/godicom-dev/godicom@latest

Clone with the optional reference submodule (test fixtures):

git clone --recurse-submodules https://github.com/godicom-dev/godicom.git

Documentation

The pkg.go.dev API reference, CHANGELOG, and PARITY coverage map vs pydicom are the primary docs. Deferred items live in TODO.

Examples

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 false

The 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 uses

NewPrivateBlock 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 absent

Reserving 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 allows

Returning 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)

Pixel Data

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")

Decompressing Pixel Data

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.

Compressing Pixel Data

err := ds.CompressPixelData(uid.RLELossless)
err = ds.CompressPixelData(uid.JPEGLSLossless)
err = ds.CompressPixelData(uid.JPEG2000Lossless)
err = ds.CompressPixelData(uid.JPEG2000) // lossy JPEG 2000

Supported 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).

DICOMDIR

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.

Overlays

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 1

Logging

godicom 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.

DICOM JSON Model

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)

Coded terminology (SR)

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, filtered

A 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 text

The 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

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 applied

RawAt 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.

CLI

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-read

show 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 support

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.

Platforms

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.

Binary size

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

Contributing

Bug reports, fixes, and documentation improvements are welcome. Please open an issue or pull request on GitHub.

License

MIT — see LICENSE.

Releases

Packages

Contributors

Languages