mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2024-04-21 12:32:08 +00:00
278 lines
8.9 KiB
Go
278 lines
8.9 KiB
Go
// Package dicom provides a set of tools to read, write, and generally
|
|
// work with DICOM (http://dicom.nema.org/) medical image files in Go.
|
|
//
|
|
// dicom.Parse and dicom.Write provide the core functionality to read and write
|
|
// DICOM Datasets. This package provides Go data structures that represent
|
|
// DICOM concepts (for example, dicom.Dataset and dicom.Element). These
|
|
// structures will pretty-print by default and are JSON serializable out of the
|
|
// box.
|
|
//
|
|
// This package provides some advanced functionality as well, including:
|
|
// streaming image frames to an output channel, reading elements one-by-one
|
|
// (like an iterator pattern), flat iteration over nested elements in a Dataset,
|
|
// and more.
|
|
//
|
|
// General usage is simple.
|
|
// Check out the package examples below and some function specific examples.
|
|
//
|
|
// It may also be helpful to take a look at the example cmd/dicomutil program,
|
|
// which is a CLI built around this library to save out image frames from DICOMs
|
|
// and print out metadata to STDOUT.
|
|
package dicom
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/suyashkumar/dicom/pkg/charset"
|
|
"github.com/suyashkumar/dicom/pkg/debug"
|
|
"github.com/suyashkumar/dicom/pkg/dicomio"
|
|
"github.com/suyashkumar/dicom/pkg/frame"
|
|
"github.com/suyashkumar/dicom/pkg/tag"
|
|
"github.com/suyashkumar/dicom/pkg/uid"
|
|
)
|
|
|
|
const (
|
|
magicWord = "DICM"
|
|
)
|
|
|
|
var (
|
|
// ErrorMagicWord indicates that the magic word was not found in the correct
|
|
// location in the DICOM.
|
|
ErrorMagicWord = errors.New("error, DICM magic word not found in correct location")
|
|
// ErrorMetaElementGroupLength indicates that the MetaElementGroupLength
|
|
// was not found where expected in the metadata.
|
|
ErrorMetaElementGroupLength = errors.New("MetaElementGroupLength tag not found where expected")
|
|
// ErrorEndOfDICOM indicates to the callers of Parser.Next() that the DICOM
|
|
// has been fully parsed. Users using one of the other Parse APIs should not
|
|
// need to use this.
|
|
ErrorEndOfDICOM = errors.New("this indicates to the caller of Next() that the DICOM has been fully parsed")
|
|
)
|
|
|
|
// Parse parses the entire DICOM at the input io.Reader into a Dataset of DICOM Elements. Use this if you are
|
|
// looking to parse the DICOM all at once, instead of element-by-element.
|
|
func Parse(in io.Reader, bytesToRead int64, frameChan chan *frame.Frame) (Dataset, error) {
|
|
p, err := NewParser(in, bytesToRead, frameChan)
|
|
if err != nil {
|
|
return Dataset{}, err
|
|
}
|
|
|
|
for !p.reader.IsLimitExhausted() {
|
|
_, err := p.Next()
|
|
if err != nil {
|
|
return p.dataset, err
|
|
}
|
|
}
|
|
|
|
// Close the frameChannel if needed
|
|
if p.frameChannel != nil {
|
|
close(p.frameChannel)
|
|
}
|
|
return p.dataset, nil
|
|
}
|
|
|
|
// ParseFile parses the entire DICOM at the given filepath. See dicom.Parse as
|
|
// well for a more generic io.Reader based API.
|
|
func ParseFile(filepath string, frameChan chan *frame.Frame) (Dataset, error) {
|
|
f, err := os.Open(filepath)
|
|
if err != nil {
|
|
return Dataset{}, err
|
|
}
|
|
defer f.Close()
|
|
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return Dataset{}, err
|
|
}
|
|
|
|
return Parse(f, info.Size(), frameChan)
|
|
}
|
|
|
|
// Parser is a struct that allows a user to parse Elements from a DICOM element-by-element using Next(), which may be
|
|
// useful for some streaming processing applications. If you instead just want to parse the whole input DICOM at once,
|
|
// just use the dicom.Parse(...) method.
|
|
type Parser struct {
|
|
reader dicomio.Reader
|
|
dataset Dataset
|
|
metadata Dataset
|
|
// file is optional, might be populated if reading from an underlying file
|
|
file *os.File
|
|
frameChannel chan *frame.Frame
|
|
}
|
|
|
|
// NewParser returns a new Parser that points to the provided io.Reader, with bytesToRead bytes left to read. NewParser
|
|
// will read the DICOM header and metadata as part of initialization.
|
|
//
|
|
// frameChannel is an optional channel (can be nil) upon which DICOM image frames will be sent as they are parsed (if
|
|
// provided).
|
|
func NewParser(in io.Reader, bytesToRead int64, frameChannel chan *frame.Frame, opts ...ParseOption) (*Parser, error) {
|
|
optSet := toParseOptSet(opts...)
|
|
reader, err := dicomio.NewReader(bufio.NewReader(in), binary.LittleEndian, bytesToRead)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p := Parser{
|
|
reader: reader,
|
|
frameChannel: frameChannel,
|
|
}
|
|
|
|
elems := []*Element{}
|
|
|
|
if !optSet.skipMetadataReadOnNewParserInit {
|
|
debug.Log("NewParser: readHeader")
|
|
elems, err = p.readHeader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
debug.Log("NewParser: readHeader complete")
|
|
}
|
|
|
|
p.dataset = Dataset{Elements: elems}
|
|
// TODO(suyashkumar): avoid storing the metadata pointers twice (though not that expensive)
|
|
p.metadata = Dataset{Elements: elems}
|
|
|
|
// Determine and set the transfer syntax based on the metadata elements parsed so far.
|
|
// The default will be LittleEndian Implicit.
|
|
var bo binary.ByteOrder = binary.LittleEndian
|
|
implicit := true
|
|
|
|
ts, err := p.dataset.FindElementByTag(tag.TransferSyntaxUID)
|
|
if err != nil {
|
|
debug.Log("WARN: could not find transfer syntax uid in metadata, proceeding with little endian implicit")
|
|
} else {
|
|
bo, implicit, err = uid.ParseTransferSyntaxUID(MustGetStrings(ts.Value)[0])
|
|
if err != nil {
|
|
// TODO(suyashkumar): should we attempt to parse with LittleEndian
|
|
// Implicit here?
|
|
debug.Log("WARN: could not parse transfer syntax uid in metadata")
|
|
}
|
|
}
|
|
p.reader.SetTransferSyntax(bo, implicit)
|
|
|
|
return &p, nil
|
|
}
|
|
|
|
// Next parses and returns the next top-level element from the DICOM this Parser points to.
|
|
func (p *Parser) Next() (*Element, error) {
|
|
if p.reader.IsLimitExhausted() {
|
|
// Close the frameChannel if needed
|
|
if p.frameChannel != nil {
|
|
close(p.frameChannel)
|
|
}
|
|
return nil, ErrorEndOfDICOM
|
|
}
|
|
elem, err := readElement(p.reader, &p.dataset, p.frameChannel)
|
|
if err != nil {
|
|
// TODO: tolerate some kinds of errors and continue parsing
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: add dicom options to only keep track of certain tags
|
|
|
|
if elem.Tag == tag.SpecificCharacterSet {
|
|
encodingNames := MustGetStrings(elem.Value)
|
|
cs, err := charset.ParseSpecificCharacterSet(encodingNames)
|
|
if err != nil {
|
|
// unable to parse character set, hard error
|
|
// TODO: add option continue, even if unable to parse
|
|
return nil, err
|
|
}
|
|
p.reader.SetCodingSystem(cs)
|
|
}
|
|
|
|
p.dataset.Elements = append(p.dataset.Elements, elem)
|
|
return elem, nil
|
|
|
|
}
|
|
|
|
// GetMetadata returns just the set of metadata elements that have been parsed
|
|
// so far.
|
|
func (p *Parser) GetMetadata() Dataset {
|
|
return p.metadata
|
|
}
|
|
|
|
// SetTransferSyntax sets the transfer syntax for the underlying dicomio.Reader.
|
|
func (p *Parser) SetTransferSyntax(bo binary.ByteOrder, implicit bool) {
|
|
p.reader.SetTransferSyntax(bo, implicit)
|
|
}
|
|
|
|
// readHeader reads the DICOM magic header and group two metadata elements.
|
|
func (p *Parser) readHeader() ([]*Element, error) {
|
|
// Check to see if magic word is at byte offset 128. If not, this is a
|
|
// non-standard non-compliant DICOM. We try to read this DICOM in a
|
|
// compatibility mode, where we rewind to position 0 and blindly attempt to
|
|
// parse a Dataset (and do not parse metadata in the usual way).
|
|
data, err := p.reader.Peek(128 + 4)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if string(data[128:]) != magicWord {
|
|
return nil, nil
|
|
}
|
|
|
|
err = p.reader.Skip(128 + 4) // skip preamble + magic word
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Must read metadata as LittleEndian explicit VR
|
|
// Read the length of the metadata elements: (0002,0000) MetaElementGroupLength
|
|
maybeMetaLen, err := readElement(p.reader, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if maybeMetaLen.Tag != tag.FileMetaInformationGroupLength || maybeMetaLen.Value.ValueType() != Ints {
|
|
return nil, ErrorMetaElementGroupLength
|
|
}
|
|
|
|
metaLen := maybeMetaLen.Value.GetValue().([]int)[0]
|
|
|
|
metaElems := []*Element{maybeMetaLen} // TODO: maybe set capacity to a reasonable initial size
|
|
|
|
// Read the metadata elements
|
|
err = p.reader.PushLimit(int64(metaLen))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer p.reader.PopLimit()
|
|
for !p.reader.IsLimitExhausted() {
|
|
elem, err := readElement(p.reader, nil, nil)
|
|
if err != nil {
|
|
// TODO: see if we can skip over malformed elements somehow
|
|
return nil, err
|
|
}
|
|
// log.Printf("Metadata Element: %s\n", elem)
|
|
metaElems = append(metaElems, elem)
|
|
}
|
|
return metaElems, nil
|
|
}
|
|
|
|
// ParseOption represents an option that can be passed to NewParser.
|
|
type ParseOption func(*parseOptSet)
|
|
|
|
// parseOptSet represents the flattened option set after all ParseOptions have been applied.
|
|
type parseOptSet struct {
|
|
skipMetadataReadOnNewParserInit bool
|
|
}
|
|
|
|
func toParseOptSet(opts ...ParseOption) *parseOptSet {
|
|
optSet := &parseOptSet{}
|
|
for _, opt := range opts {
|
|
opt(optSet)
|
|
}
|
|
return optSet
|
|
}
|
|
|
|
// SkipMetadataReadOnNewParserInit makes NewParser skip trying to parse metadata. This will make the Parser default to implicit little endian byte order.
|
|
// Any metatata tags found in the dataset will still be available when parsing.
|
|
func SkipMetadataReadOnNewParserInit() ParseOption {
|
|
return func(set *parseOptSet) {
|
|
set.skipMetadataReadOnNewParserInit = true
|
|
}
|
|
}
|