diff --git a/dicom.go b/dicom.go deleted file mode 100755 index 2c30acc..0000000 --- a/dicom.go +++ /dev/null @@ -1,170 +0,0 @@ -package dicom - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - "os" - - "github.com/gradienthealth/go-dicom/dicomio" - "github.com/gradienthealth/go-dicom/dicomtag" -) - -// GoDICOMImplementationClassUIDPrefix defines the UID prefix for -// go-dicom. Provided by https://www.medicalconnections.co.uk/Free_UID -const GoDICOMImplementationClassUIDPrefix = "1.2.826.0.1.3680043.9.7133" - -var GoDICOMImplementationClassUID = GoDICOMImplementationClassUIDPrefix + ".1.1" - -const GoDICOMImplementationVersionName = "GODICOM_1_1" - -// DataSet represents contents of one DICOM file. -type DataSet struct { - // Elements in the file, in order of appearance. - // - // Note: unlike pydicom, Elements also contains meta elements (those - // with Tag.Group==2). - Elements []*Element -} - -func doassert(cond bool, values ...interface{}) { - if !cond { - var s string - for _, value := range values { - s += fmt.Sprintf("%v ", value) - } - panic(s) - } -} - -// ReadOptions defines how DataSets and Elements are parsed. -type ReadOptions struct { - // DropPixelData will cause the parser to skip the PixelData element - // (bulk images) in ReadDataSet. - DropPixelData bool - - // ReturnTags is a whitelist of tags to return. - ReturnTags []dicomtag.Tag - - // StopAtag defines a tag at which when read (or a tag with a greater - // value than it is read), the program will stop parsing the dicom file. - StopAtTag *dicomtag.Tag -} - -// ReadDataSetInBytes is a shorthand for ReadDataSet(bytes.NewBuffer(data), len(data)). -// -// On parse error, this function may return a non-nil dataset and a non-nil -// error. In such case, the dataset will contain parts of the file that are -// parsable, and error will show the first error found by the parser. -func ReadDataSetInBytes(data []byte, options ReadOptions) (*DataSet, error) { - return ReadDataSet(bytes.NewBuffer(data), int64(len(data)), options) -} - -// ReadDataSetFromFile parses file cotents into dicom.DataSet. It is a thin -// wrapper around ReadDataSet. -// -// On parse error, this function may return a non-nil dataset and a non-nil -// error. In such case, the dataset will contain parts of the file that are -// parsable, and error will show the first error found by the parser. -func ReadDataSetFromFile(path string, options ReadOptions) (*DataSet, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - st, err := file.Stat() - if err != nil { - return nil, err - } - return ReadDataSet(file, st.Size(), options) -} - -// ReadDataSet reads a DICOM file from "io", up to "bytes". -// -// On parse error, this function may return a non-nil dataset and a non-nil -// error. In such case, the dataset will contain parts of the file that are -// parsable, and error will show the first error found by the parser. -func ReadDataSet(in io.Reader, bytes int64, options ReadOptions) (*DataSet, error) { - buffer := dicomio.NewDecoder(in, bytes, binary.LittleEndian, dicomio.ExplicitVR) - metaElems := ParseFileHeader(buffer) - if buffer.Error() != nil { - return nil, buffer.Error() - } - file := &DataSet{Elements: metaElems} - - // Change the transfer syntax for the rest of the file. - endian, implicit, err := getTransferSyntax(file) - if err != nil { - return nil, err - } - buffer.PushTransferSyntax(endian, implicit) - defer buffer.PopTransferSyntax() - - // Read the list of elements. - for buffer.Len() > 0 { - startLen := buffer.Len() - elem := ReadElement(buffer, file, options) - if buffer.Len() >= startLen { // Avoid silent infinite looping. - panic(fmt.Sprintf("ReadElement failed to consume data: %d %d: %v", startLen, buffer.Len(), buffer.Error())) - } - if elem == endOfDataElement { - // element is a pixel data and was dropped by options - break - } - if elem == nil { - // Parse error. - continue - } - if elem.Tag == dicomtag.SpecificCharacterSet { - // Set the []byte -> string decoder for the rest of the - // file. It's sad that SpecificCharacterSet isn't part - // of metadata, but is part of regular attrs, so we need - // to watch out for multiple occurrences of this type of - // elements. - encodingNames, err := elem.GetStrings() - if err != nil { - buffer.SetError(err) - } else { - // TODO(saito) SpecificCharacterSet may appear in a - // middle of a SQ or NA. In such case, the charset seem - // to be scoped inside the SQ or NA. So we need to make - // the charset a stack. - cs, err := dicomio.ParseSpecificCharacterSet(encodingNames) - if err != nil { - buffer.SetError(err) - } else { - buffer.SetCodingSystem(cs) - } - } - } - if options.ReturnTags == nil || (options.ReturnTags != nil && tagInList(elem.Tag, options.ReturnTags)) { - file.Elements = append(file.Elements, elem) - } - } - return file, buffer.Error() -} - -func getTransferSyntax(ds *DataSet) (bo binary.ByteOrder, implicit dicomio.IsImplicitVR, err error) { - elem, err := ds.FindElementByTag(dicomtag.TransferSyntaxUID) - if err != nil { - return nil, dicomio.UnknownVR, err - } - transferSyntaxUID, err := elem.GetString() - if err != nil { - return nil, dicomio.UnknownVR, err - } - return dicomio.ParseTransferSyntaxUID(transferSyntaxUID) -} - -// FindElementByName finds an element from the dataset given the element name, -// such as "PatientName". -func (f *DataSet) FindElementByName(name string) (*Element, error) { - return FindElementByName(f.Elements, name) -} - -// FindElementByTag finds an element from the dataset given its tag, such as -// Tag{0x0010, 0x0010}. -func (f *DataSet) FindElementByTag(tag dicomtag.Tag) (*Element, error) { - return FindElementByTag(f.Elements, tag) -} \ No newline at end of file diff --git a/dicomdir.go b/dicomdir.go index 146056a..eb27aeb 100644 --- a/dicomdir.go +++ b/dicomdir.go @@ -22,7 +22,11 @@ func ParseDICOMDIR(in io.Reader) (recs []DirectoryRecord, err error) { if err != nil { return nil, err } - ds, err := ReadDataSetInBytes(bytes, ReadOptions{}) + p, err := NewParserFromBytes(bytes, nil) + if err != nil { + return nil, err + } + ds, err := p.Parse(ParseOptions{}) if err != nil { return nil, err } diff --git a/dicomutil/dicomutil.go b/dicomutil/dicomutil.go index 5698561..7fcb33b 100644 --- a/dicomutil/dicomutil.go +++ b/dicomutil/dicomutil.go @@ -33,9 +33,13 @@ func main() { dicomlog.SetLevel(math.MaxInt32) } path := flag.Arg(0) - parsedData, err := dicom.ReadDataSetFromFile(path, dicom.ReadOptions{DropPixelData: !*extractImages}) - if parsedData == nil { - log.Panic("Error reading %s: %v", path, err) + p, err := dicom.NewParserFromFile(path, nil) + if err != nil { + log.Panic("error creating new parser", err) + } + parsedData, err := p.Parse(dicom.ParseOptions{DropPixelData: !*extractImages}) + if parsedData == nil || err != nil { + log.Panicf("Error reading %s: %v", path, err) } if *printMetadata { for _, elem := range parsedData.Elements { diff --git a/element.go b/element.go index 03eef1e..641f820 100644 --- a/element.go +++ b/element.go @@ -3,15 +3,11 @@ package dicom import ( "crypto/sha256" "encoding/base64" - "encoding/binary" "errors" "fmt" "strings" - "strconv" - "github.com/gradienthealth/go-dicom/dicomio" - "github.com/gradienthealth/go-dicom/dicomlog" "github.com/gradienthealth/go-dicom/dicomtag" ) @@ -148,7 +144,7 @@ func MustNewElement(tag dicomtag.Tag, values ...interface{}) *Element { // element contains zero or >1 values, or the value is not a uint32. func (e *Element) GetUInt32() (uint32, error) { if len(e.Value) != 1 { - return 0, fmt.Errorf("Found %d value(s) in getuint32 (expect 1): %v", len(e.Value), e) + return 0, fmt.Errorf("Found %decoder value(s) in getuint32 (expect 1): %v", len(e.Value), e) } v, ok := e.Value[0].(uint32) if !ok { @@ -170,7 +166,7 @@ func (e *Element) MustGetUInt32() uint32 { // element contains zero or >1 values, or the value is not a uint16. func (e *Element) GetUInt16() (uint16, error) { if len(e.Value) != 1 { - return 0, fmt.Errorf("Found %d value(s) in getuint16 (expect 1): %v", len(e.Value), e) + return 0, fmt.Errorf("Found %decoder value(s) in getuint16 (expect 1): %v", len(e.Value), e) } v, ok := e.Value[0].(uint16) if !ok { @@ -192,7 +188,7 @@ func (e *Element) MustGetUInt16() uint16 { // element contains zero or >1 values, or the value is not a string. func (e *Element) GetString() (string, error) { if len(e.Value) != 1 { - return "", fmt.Errorf("Found %d value(s) in getstring (expect 1): %v", len(e.Value), e.String()) + return "", fmt.Errorf("Found %decoder value(s) in getstring (expect 1): %v", len(e.Value), e.String()) } v, ok := e.Value[0].(string) if !ok { @@ -291,7 +287,7 @@ func elementString(e *Element, nestLevel int) string { } s = fmt.Sprintf("%s %s %s %s ", s, dicomtag.DebugString(e.Tag), e.VR, sVl) if e.VR == "SQ" || e.Tag == dicomtag.Item { - s += fmt.Sprintf(" (#%d)[\n", len(e.Value)) + s += fmt.Sprintf(" (#%decoder)[\n", len(e.Value)) for _, v := range e.Value { s += elementString(v.(*Element), nestLevel+1) + "\n" } @@ -301,7 +297,7 @@ func elementString(e *Element, nestLevel int) string { if len(e.Value) == 1 { sv = fmt.Sprintf("%v", e.Value) } else { - sv = fmt.Sprintf("(%d)%v", len(e.Value), e.Value) + sv = fmt.Sprintf("(%decoder)%v", len(e.Value), e.Value) } if len(sv) > 1024 { sv = sv[1:1024] + "(...)" @@ -359,7 +355,7 @@ func (data PixelDataInfo) String() string { s := fmt.Sprintf("image{offsets: %v, encapsulated frames: [", data.Offsets) for i := 0; i < len(data.EncapsulatedFrames); i++ { csum := sha256.Sum256(data.EncapsulatedFrames[i]) - s += fmt.Sprintf("%d:{size:%d, csum:%v}, ", + s += fmt.Sprintf("%decoder:{size:%decoder, csum:%v}, ", i, len(data.EncapsulatedFrames[i]), base64.URLEncoding.EncodeToString(csum[:])) } @@ -367,7 +363,7 @@ func (data PixelDataInfo) String() string { s += "], native frames: [" for i := 0; i < len(data.NativeFrames); i++ { - s += fmt.Sprintf("%d:{size:%d}, ", + s += fmt.Sprintf("%decoder:{size:%decoder}, ", i, len(data.NativeFrames[i])) } @@ -396,309 +392,9 @@ func readBasicOffsetTable(d *dicomio.Decoder) []uint32 { return offsets } -// ParseFileHeader consumes the DICOM magic header and metadata elements (whose -// elements with tag group==2) from a Dicom file. Errors are reported through -// d.Error(). -func ParseFileHeader(d *dicomio.Decoder) []*Element { - d.PushTransferSyntax(binary.LittleEndian, dicomio.ExplicitVR) - defer d.PopTransferSyntax() - d.Skip(128) // skip preamble - - // check for magic word - if s := d.ReadString(4); s != "DICM" { - d.SetError(errors.New("Keyword 'DICM' not found in the header")) - return nil - } - - // (0002,0000) MetaElementGroupLength - metaElem := ReadElement(d, nil, ReadOptions{}) - if d.Error() != nil { - return nil - } - if metaElem.Tag != dicomtag.FileMetaInformationGroupLength { - d.SetErrorf("MetaElementGroupLength not found; insteadfound %s", metaElem.Tag.String()) - } - metaLength, err := metaElem.GetUInt32() - if err != nil { - d.SetErrorf("Failed to read uint32 in MetaElementGroupLength: %v", err) - return nil - } - if d.Len() <= 0 { - d.SetErrorf("No data element found") - return nil - } - metaElems := []*Element{metaElem} - - // Read meta tags - d.PushLimit(int64(metaLength)) - defer d.PopLimit() - for d.Len() > 0 { - elem := ReadElement(d, nil, ReadOptions{}) - if d.Error() != nil { - break - } - metaElems = append(metaElems, elem) - dicomlog.Vprintf(2, "dicom.ParseFileHeader: Meta elem: %v, len %v", elem.String(), d.Len()) - } - return metaElems -} - // endElement is an pseudoelement to cause the caller to stop reading the input. var endOfDataElement = &Element{Tag: dicomtag.Tag{Group: 0x7fff, Element: 0x7fff}} -// ReadElement reads one DICOM data element. The parsedData ref must only be provided when potentially reading PixelData, -// otherwise can be nil. ReadElement returns three kind of values. -// -// - On parse error, it returns nil and sets the error in d.Error(). -// -// - It returns (endOfDataElement, nil) if options.DropPixelData=true and the -// element is a pixel data, or it sees an element defined by options.StopAtTag. -// -// - On successful parsing, it returns non-nil and non-endOfDataElement value. -func ReadElement(d *dicomio.Decoder, parsedData *DataSet, options ReadOptions) *Element { - tag := readTag(d) - if tag == dicomtag.PixelData && options.DropPixelData { - return endOfDataElement - } - - // Return nil if the tag is greater than the StopAtTag if a StopAtTag is given - if options.StopAtTag != nil && tag.Group >= options.StopAtTag.Group && tag.Element >= options.StopAtTag.Element { - return endOfDataElement - } - // The elements for group 0xFFFE should be Encoded as Implicit VR. - // DICOM Standard 09. PS 3.6 - Section 7.5: "Nesting of Data Sets" - _, implicit := d.TransferSyntax() - if tag.Group == itemSeqGroup { - implicit = dicomio.ImplicitVR - } - var vr string // Value Representation - var vl uint32 // Value Length - if implicit == dicomio.ImplicitVR { - vr, vl = readImplicit(d, tag) - } else { - doassert(implicit == dicomio.ExplicitVR, implicit) - vr, vl = readExplicit(d, tag) - } - var data []interface{} - - elem := &Element{ - Tag: tag, - VR: vr, - UndefinedLength: (vl == undefinedLength), - } - if vr == "UN" && vl == undefinedLength { - // This combination appears in some file, but it's unclear what - // to do. The standard, as always, is unclear. The best guess is - // in PS3.5, 6.2.2, where it states that the combination of - // vr=UN and undefined length is allowed, it refers to a section - // of parsing "Data Elemets with Unknown Length". That reference - // is specifically about element of type SQ, so I'm just - // assuming is the same as . - vr = "SQ" - } - if tag == dicomtag.PixelData { - // P3.5, A.4 describes the format. Currently we only support an encapsulated image format. - // - // PixelData is usually the last element in a DICOM file. When - // the file stores N images, the elements that follow PixelData - // are laid out in the following way: - // - // Item(BasicOffsetTable) Item(PixelDataInfo0) ... Item(PixelDataInfoM) SequenceDelimiterItem - // - // Item(BasicOffsetTable) is an Item element whose payload - // encodes N uint32 values. Kth uint32 is the bytesize of the - // Kth image. Item(PixelDataInfo*) are chunked sequences of bytes. I - // presume that single PixelDataInfo item doesn't cross a image - // boundary, but the spec isn't clear. - // - // The total byte size of Item(PixelDataInfo*) equal the total of - // the bytesizes found in BasicOffsetTable. - if vl == undefinedLength { - var image PixelDataInfo - image.Encapsulated = true - image.Offsets = readBasicOffsetTable(d) // TODO(saito) Use the offset table. - if len(image.Offsets) > 1 { - dicomlog.Vprintf(1, "dicom.ReadElement: Multiple images not supported yet. Combining them into a byte sequence: %v", image.Offsets) - } - for d.Len() > 0 { - chunk, endOfItems := readRawItem(d) - if d.Error() != nil { - break - } - if endOfItems { - break - } - image.EncapsulatedFrames = append(image.EncapsulatedFrames, chunk) - } - data = append(data, image) - } else { - // Assume we're reading Native data since we have a defined value length as per Part 5 Sec A.4 of DICOM spec. - // We need Elements that have been already parsed (rows, cols, etc) to parse frames out of Native Pixel data - if parsedData == nil { - d.SetError(errors.New("dicom.ReadElement: parsedData is nil, must exist to parse Native pixel data")) - return nil // TODO(suyash) investigate error handling in this library - } - - image, _, err := readNativeFrames(d, parsedData) - - if err != nil { - d.SetError(err) - dicomlog.Vprintf(1, "dicom.ReadElement: Error reading native frames") - return nil - } - - data = append(data, *image) - } - } else if vr == "SQ" { - // Note: when reading subitems inside sequence or item, we ignore - // DropPixelData and other shortcircuiting options. If we honored them, we'd - // be unable to read the rest of the file. - if vl == undefinedLength { - // Format: - // Sequence := ItemSet* SequenceDelimitationItem - // ItemSet := Item Any* ItemDelimitationItem (when Item.VL is undefined) or - // Item Any*N (when Item.VL has a defined value) - for { - // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag - item := ReadElement(d, parsedData, ReadOptions{}) - if d.Error() != nil { - break - } - if item.Tag == dicomtag.SequenceDelimitationItem { - break - } - if item.Tag != dicomtag.Item { - d.SetErrorf("dicom.ReadElement: Found non-Item element in seq w/ undefined length: %v", dicomtag.DebugString(item.Tag)) - break - } - data = append(data, item) - } - } else { - // Format: - // Sequence := ItemSet*VL - // See the above comment for the definition of ItemSet. - d.PushLimit(int64(vl)) - for d.Len() > 0 { - // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag - item := ReadElement(d, parsedData, ReadOptions{}) - if d.Error() != nil { - break - } - if item.Tag != dicomtag.Item { - d.SetErrorf("dicom.ReadElement: Found non-Item element in seq w/ undefined length: %v", dicomtag.DebugString(item.Tag)) - break - } - data = append(data, item) - } - d.PopLimit() - } - } else if tag == dicomtag.Item { // Item (component of SQ) - if vl == undefinedLength { - // Format: Item Any* ItemDelimitationItem - for { - // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag - subelem := ReadElement(d, parsedData, ReadOptions{}) - if d.Error() != nil { - break - } - if subelem.Tag == dicomtag.ItemDelimitationItem { - break - } - data = append(data, subelem) - } - } else { - // Sequence of arbitary elements, for the total of "vl" bytes. - d.PushLimit(int64(vl)) - for d.Len() > 0 { - // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag - subelem := ReadElement(d, parsedData, ReadOptions{}) - if d.Error() != nil { - break - } - data = append(data, subelem) - } - d.PopLimit() - } - } else { // List of scalar - if vl == undefinedLength { - d.SetErrorf("dicom.ReadElement: Undefined length disallowed for VR=%s, tag %s", vr, dicomtag.DebugString(tag)) - return nil - } - d.PushLimit(int64(vl)) - defer d.PopLimit() - if vr == "DA" { - // TODO(saito) Maybe we should validate the date. - date := strings.Trim(d.ReadString(int(vl)), " \000") - data = []interface{}{date} - } else if vr == "AT" { - // (2byte group, 2byte elem) - for d.Len() > 0 && d.Error() == nil { - tag := dicomtag.Tag{d.ReadUInt16(), d.ReadUInt16()} - data = append(data, tag) - } - } else if vr == "OW" { - if vl%2 != 0 { - d.SetErrorf("dicom.ReadElement: tag %v: OW requires even length, but found %v", dicomtag.DebugString(tag), vl) - } else { - n := int(vl / 2) - e := dicomio.NewBytesEncoder(dicomio.NativeByteOrder, dicomio.UnknownVR) - for i := 0; i < n; i++ { - v := d.ReadUInt16() - e.WriteUInt16(v) - } - doassert(e.Error() == nil, e.Error()) - // TODO(saito) Check that size is even. Byte swap?? - // TODO(saito) If OB's length is odd, is VL odd too? Need to check! - data = append(data, e.Bytes()) - } - } else if vr == "OB" { - // TODO(saito) Check that size is even. Byte swap?? - // TODO(saito) If OB's length is odd, is VL odd too? Need to check! - data = append(data, d.ReadBytes(int(vl))) - } else if vr == "LT" || vr == "UT" { - str := d.ReadString(int(vl)) - data = append(data, str) - } else if vr == "UL" { - for d.Len() > 0 && d.Error() == nil { - data = append(data, d.ReadUInt32()) - } - } else if vr == "SL" { - for d.Len() > 0 && d.Error() == nil { - data = append(data, d.ReadInt32()) - } - } else if vr == "US" { - for d.Len() > 0 && d.Error() == nil { - data = append(data, d.ReadUInt16()) - } - } else if vr == "SS" { - for d.Len() > 0 && d.Error() == nil { - data = append(data, d.ReadInt16()) - } - } else if vr == "FL" { - for d.Len() > 0 && d.Error() == nil { - data = append(data, d.ReadFloat32()) - } - } else if vr == "FD" { - for d.Len() > 0 && d.Error() == nil { - data = append(data, d.ReadFloat64()) - } - } else { - // List of strings, each delimited by '\\'. - v := d.ReadString(int(vl)) - // String may have '\0' suffix if its length is odd. - str := strings.Trim(v, " \000") - if len(str) > 0 { - for _, s := range strings.Split(str, "\\") { - data = append(data, s) - } - } - } - } - elem.Value = data - return elem -} - const undefinedLength uint32 = 0xffffffff // Read a DICOM data element's tag value ie. (0002,0000) added Value @@ -755,81 +451,6 @@ func readExplicit(buffer *dicomio.Decoder, tag dicomtag.Tag) (string, uint32) { return vr, vl } -// readNativeFrames reads Native frames from a Decoder based on already parsed pixel information -// that should be available in parsedData (elements like NumberOfFrames, rows, columns, etc) -func readNativeFrames(d *dicomio.Decoder, parsedData *DataSet) (pixelData *PixelDataInfo, bytesRead int, err error) { - image := PixelDataInfo{ - Encapsulated: false, - } - - // Parse information from previously parsed attributes that are needed to parse Native Frames: - rows, err := parsedData.FindElementByTag(dicomtag.Rows) - if err != nil { - return nil, 0, err - } - - cols, err := parsedData.FindElementByTag(dicomtag.Columns) - if err != nil { - return nil, 0, err - } - - nof, err := parsedData.FindElementByTag(dicomtag.NumberOfFrames) - nFrames := 0 - if err == nil { - // No error, so parse number of frames - nFrames, err = strconv.Atoi(nof.MustGetString()) // odd that number of frames is encoded as a string... - if err != nil { - dicomlog.Vprintf(1, "ERROR converting nof") - return nil, 0, err - } - } else { - // error fetching NumberOfFrames, so default to 1. TODO: revisit - nFrames = 1 - } - - b, err := parsedData.FindElementByTag(dicomtag.BitsAllocated) - if err != nil { - dicomlog.Vprintf(1, "ERROR finding bits allocated.") - return nil, 0, err - } - bitsAllocated := int(b.MustGetUInt16()) - image.BitsPerSample = bitsAllocated - - s, err := parsedData.FindElementByTag(dicomtag.SamplesPerPixel) - if err != nil { - dicomlog.Vprintf(1, "ERROR finding samples per pixel") - } - samplesPerPixel := int(s.MustGetUInt16()) - - pixelsPerFrame := int(rows.MustGetUInt16()) * int(cols.MustGetUInt16()) - - dicomlog.Vprintf(1, "Image size: %d x %d", rows.MustGetUInt16(), cols.MustGetUInt16()) - dicomlog.Vprintf(1, "Pixels Per Frame: %d", pixelsPerFrame) - dicomlog.Vprintf(1, "Number of frames %d", nFrames) - - // Parse the pixels: - image.NativeFrames = make([][][]int, nFrames) - for frame := 0; frame < nFrames; frame++ { - currentFrame := make([][]int, pixelsPerFrame) - for pixel := 0; pixel < int(pixelsPerFrame); pixel++ { - currentPixel := make([]int, samplesPerPixel) - for value := 0; value < samplesPerPixel; value++ { - if bitsAllocated == 8 { - currentPixel[value] = int(d.ReadUInt8()) - } else if bitsAllocated == 16 { - currentPixel[value] = int(d.ReadUInt16()) - } - } - currentFrame[pixel] = currentPixel - } - image.NativeFrames[frame] = currentFrame - } - - bytesRead = (bitsAllocated / 8) * samplesPerPixel * pixelsPerFrame * nFrames - - return &image, bytesRead, nil -} - func tagInList(tag dicomtag.Tag, tags []dicomtag.Tag) bool { for _, t := range tags { if tag == t { diff --git a/fuzztest/fuzz.go b/fuzztest/fuzz.go index 2ec8a6f..bb8069c 100644 --- a/fuzztest/fuzz.go +++ b/fuzztest/fuzz.go @@ -1,13 +1,12 @@ package fuzz import ( - "bytes" - "github.com/gradienthealth/go-dicom" ) func Fuzz(data []byte) int { - _, _ = dicom.ReadDataSet(bytes.NewBuffer(data), int64(len(data)), - dicom.ReadOptions{}) + p, _ := dicom.NewParserFromBytes(data, nil) + _, _ = p.Parse(dicom.ParseOptions{}) + return 1 } diff --git a/parse.go b/parse.go new file mode 100755 index 0000000..037ce3d --- /dev/null +++ b/parse.go @@ -0,0 +1,576 @@ +package dicom + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/gradienthealth/go-dicom/dicomio" + "github.com/gradienthealth/go-dicom/dicomlog" + "github.com/gradienthealth/go-dicom/dicomtag" +) + +// GoDICOMImplementationClassUIDPrefix defines the UID prefix for +// go-dicom. Provided by https://www.medicalconnections.co.uk/Free_UID +const GoDICOMImplementationClassUIDPrefix = "1.2.826.0.1.3680043.9.7133" + +var GoDICOMImplementationClassUID = GoDICOMImplementationClassUIDPrefix + ".1.1" + +const GoDICOMImplementationVersionName = "GODICOM_1_1" + +// Parser represents an entity that can read and parse DICOMs +type Parser interface { + // Parse DICOM data + Parse(options ParseOptions) (*DataSet, error) + // ParseNext reads and parses the next element + ParseNext(options ParseOptions) *Element + // DecoderError fetches an error (if exists) from the dicomio.Decoder + DecoderError() error // This should go away as we continue refactors + // Finish should be called after manually parsing elements using ParseNext (instead of Parse) + Finish() error // This should maybe go away as we continue refactors +} + +// parser implements Parser +type parser struct { + decoder *dicomio.Decoder + parsedElements *DataSet + op ParseOptions + frameChannel chan [][]int + file *os.File // may be populated if coming from file +} + +// NewParser initializes and returns a new Parser +func NewParser(in io.Reader, bytesToRead int64, frameChannel chan [][]int) (Parser, error) { + buffer := dicomio.NewDecoder(in, bytesToRead, binary.LittleEndian, dicomio.ExplicitVR) + p := parser{ + decoder: buffer, + frameChannel: frameChannel, + } + + metaElems := p.parseFileHeader() + if buffer.Error() != nil { + return nil, buffer.Error() + } + parsedElements := &DataSet{Elements: metaElems} + p.parsedElements = parsedElements + return &p, nil +} + +// NewParserFromBytes initializes and returns a new Parser from []byte +func NewParserFromBytes(data []byte, frameChannel chan [][]int) (Parser, error) { + return NewParser(bytes.NewBuffer(data), int64(len(data)), frameChannel) +} + +// NewParserFromFile initializes and returns a new dicom Parser from a file path +func NewParserFromFile(path string, frameChannel chan [][]int) (Parser, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + st, err := file.Stat() + if err != nil { + return nil, err + } + p, err := NewParser(file, st.Size(), frameChannel) + p.(*parser).file = file + return p, err +} + +func (p *parser) Parse(options ParseOptions) (*DataSet, error) { + // Change the transfer syntax for the rest of the file. + endian, implicit, err := getTransferSyntax(p.parsedElements) + if err != nil { + return nil, err + } + p.decoder.PushTransferSyntax(endian, implicit) + defer p.decoder.PopTransferSyntax() + + // if reading from file, close the file after done parsing + if p.file != nil { + defer p.file.Close() + } + + // Read the list of elements. + for p.decoder.Len() > 0 { + startLen := p.decoder.Len() + elem := p.ParseNext(options) + if p.decoder.Len() >= startLen { // Avoid silent infinite looping. + panic(fmt.Sprintf("ReadElement failed to consume data: %decoder %decoder: %v", startLen, p.decoder.Len(), p.decoder.Error())) + } + if elem == endOfDataElement { + // element is a pixel data and was dropped by options + break + } + if elem == nil { + // Parse error. + continue + } + if elem.Tag == dicomtag.SpecificCharacterSet { + // Set the []byte -> string decoder for the rest of the + // file. It's sad that SpecificCharacterSet isn't part + // of metadata, but is part of regular attrs, so we need + // to watch out for multiple occurrences of this type of + // elements. + encodingNames, err := elem.GetStrings() + if err != nil { + p.decoder.SetError(err) + } else { + // TODO(saito) SpecificCharacterSet may appear in a + // middle of a SQ or NA. In such case, the charset seem + // to be scoped inside the SQ or NA. So we need to make + // the charset a stack. + cs, err := dicomio.ParseSpecificCharacterSet(encodingNames) + if err != nil { + p.decoder.SetError(err) + } else { + p.decoder.SetCodingSystem(cs) + } + } + } + if options.ReturnTags == nil || (options.ReturnTags != nil && tagInList(elem.Tag, options.ReturnTags)) { + p.parsedElements.Elements = append(p.parsedElements.Elements, elem) + } + + } + return p.parsedElements, p.decoder.Error() +} + +func (p *parser) ParseNext(options ParseOptions) *Element { + tag := readTag(p.decoder) + if tag == dicomtag.PixelData && options.DropPixelData { + return endOfDataElement + } + + // Return nil if the tag is greater than the StopAtTag if a StopAtTag is given + if options.StopAtTag != nil && tag.Group >= options.StopAtTag.Group && tag.Element >= options.StopAtTag.Element { + return endOfDataElement + } + // The elements for group 0xFFFE should be Encoded as Implicit VR. + // DICOM Standard 09. PS 3.6 - Section 7.5: "Nesting of Data Sets" + _, implicit := p.decoder.TransferSyntax() + if tag.Group == itemSeqGroup { + implicit = dicomio.ImplicitVR + } + var vr string // Value Representation + var vl uint32 // Value Length + if implicit == dicomio.ImplicitVR { + vr, vl = readImplicit(p.decoder, tag) + } else { + doassert(implicit == dicomio.ExplicitVR, implicit) + vr, vl = readExplicit(p.decoder, tag) + } + var data []interface{} + + elem := &Element{ + Tag: tag, + VR: vr, + UndefinedLength: vl == undefinedLength, + } + if vr == "UN" && vl == undefinedLength { + // This combination appears in some file, but it's unclear what + // to do. The standard, as always, is unclear. The best guess is + // in PS3.5, 6.2.2, where it states that the combination of + // vr=UN and undefined length is allowed, it refers to a section + // of parsing "Data Elemets with Unknown Length". That reference + // is specifically about element of type SQ, so I'm just + // assuming is the same as . + vr = "SQ" + } + if tag == dicomtag.PixelData { + // P3.5, A.4 describes the format. Currently we only support an encapsulated image format. + // + // PixelData is usually the last element in a DICOM file. When + // the file stores N images, the elements that follow PixelData + // are laid out in the following way: + // + // Item(BasicOffsetTable) Item(PixelDataInfo0) ... Item(PixelDataInfoM) SequenceDelimiterItem + // + // Item(BasicOffsetTable) is an Item element whose payload + // encodes N uint32 values. Kth uint32 is the bytesize of the + // Kth image. Item(PixelDataInfo*) are chunked sequences of bytes. I + // presume that single PixelDataInfo item doesn't cross a image + // boundary, but the spec isn't clear. + // + // The total byte size of Item(PixelDataInfo*) equal the total of + // the bytesizes found in BasicOffsetTable. + if vl == undefinedLength { + var image PixelDataInfo + image.Encapsulated = true + image.Offsets = readBasicOffsetTable(p.decoder) // TODO(saito) Use the offset table. + if len(image.Offsets) > 1 { + dicomlog.Vprintf(1, "dicom.ReadElement: Multiple images not supported yet. Combining them into a byte sequence: %v", image.Offsets) + } + for p.decoder.Len() > 0 { + chunk, endOfItems := readRawItem(p.decoder) + if p.decoder.Error() != nil { + break + } + if endOfItems { + break + } + image.EncapsulatedFrames = append(image.EncapsulatedFrames, chunk) + } + data = append(data, image) + } else { + // Assume we're reading Native data since we have a defined value length as per Part 5 Sec A.4 of DICOM spec. + // We need Elements that have been already parsed (rows, cols, etc) to parse frames out of Native Pixel data + if p.parsedElements == nil { + p.decoder.SetError(errors.New("dicom.ReadElement: parsedData is nil, must exist to parse Native pixel data")) + return nil // TODO(suyash) investigate error handling in this library + } + + image, _, err := readNativeFrames(p.decoder, p.parsedElements) + + if err != nil { + p.decoder.SetError(err) + dicomlog.Vprintf(1, "dicom.ReadElement: Error reading native frames") + return nil + } + + data = append(data, *image) + } + } else if vr == "SQ" { + // Note: when reading subitems inside sequence or item, we ignore + // DropPixelData and other shortcircuiting options. If we honored them, we'decoder + // be unable to read the rest of the file. + if vl == undefinedLength { + // Format: + // Sequence := ItemSet* SequenceDelimitationItem + // ItemSet := Item Any* ItemDelimitationItem (when Item.VL is undefined) or + // Item Any*N (when Item.VL has a defined value) + for { + // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag + item := p.ParseNext(ParseOptions{}) + if p.decoder.Error() != nil { + break + } + if item.Tag == dicomtag.SequenceDelimitationItem { + break + } + if item.Tag != dicomtag.Item { + p.decoder.SetErrorf("dicom.ReadElement: Found non-Item element in seq w/ undefined length: %v", dicomtag.DebugString(item.Tag)) + break + } + data = append(data, item) + } + } else { + // Format: + // Sequence := ItemSet*VL + // See the above comment for the definition of ItemSet. + p.decoder.PushLimit(int64(vl)) + for p.decoder.Len() > 0 { + // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag + item := p.ParseNext(ParseOptions{}) + if p.decoder.Error() != nil { + break + } + if item.Tag != dicomtag.Item { + p.decoder.SetErrorf("dicom.ReadElement: Found non-Item element in seq w/ undefined length: %v", dicomtag.DebugString(item.Tag)) + break + } + data = append(data, item) + } + p.decoder.PopLimit() + } + } else if tag == dicomtag.Item { // Item (component of SQ) + if vl == undefinedLength { + // Format: Item Any* ItemDelimitationItem + for { + // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag + subelem := p.ParseNext(ParseOptions{}) + if p.decoder.Error() != nil { + break + } + if subelem.Tag == dicomtag.ItemDelimitationItem { + break + } + data = append(data, subelem) + } + } else { + // Sequence of arbitary elements, for the total of "vl" bytes. + p.decoder.PushLimit(int64(vl)) + for p.decoder.Len() > 0 { + // Makes sure to return all sub elements even if the tag is not in the return tags list of options or is greater than the Stop At Tag + subelem := p.ParseNext(ParseOptions{}) + if p.decoder.Error() != nil { + break + } + data = append(data, subelem) + } + p.decoder.PopLimit() + } + } else { // List of scalar + if vl == undefinedLength { + p.decoder.SetErrorf("dicom.ReadElement: Undefined length disallowed for VR=%s, tag %s", vr, dicomtag.DebugString(tag)) + return nil + } + p.decoder.PushLimit(int64(vl)) + defer p.decoder.PopLimit() + if vr == "DA" { + // TODO(saito) Maybe we should validate the date. + date := strings.Trim(p.decoder.ReadString(int(vl)), " \000") + data = []interface{}{date} + } else if vr == "AT" { + // (2byte group, 2byte elem) + for p.decoder.Len() > 0 && p.decoder.Error() == nil { + tag := dicomtag.Tag{p.decoder.ReadUInt16(), p.decoder.ReadUInt16()} + data = append(data, tag) + } + } else if vr == "OW" { + if vl%2 != 0 { + p.decoder.SetErrorf("dicom.ReadElement: tag %v: OW requires even length, but found %v", dicomtag.DebugString(tag), vl) + } else { + n := int(vl / 2) + e := dicomio.NewBytesEncoder(dicomio.NativeByteOrder, dicomio.UnknownVR) + for i := 0; i < n; i++ { + v := p.decoder.ReadUInt16() + e.WriteUInt16(v) + } + doassert(e.Error() == nil, e.Error()) + // TODO(saito) Check that size is even. Byte swap?? + // TODO(saito) If OB's length is odd, is VL odd too? Need to check! + data = append(data, e.Bytes()) + } + } else if vr == "OB" { + // TODO(saito) Check that size is even. Byte swap?? + // TODO(saito) If OB's length is odd, is VL odd too? Need to check! + data = append(data, p.decoder.ReadBytes(int(vl))) + } else if vr == "LT" || vr == "UT" { + str := p.decoder.ReadString(int(vl)) + data = append(data, str) + } else if vr == "UL" { + for p.decoder.Len() > 0 && p.decoder.Error() == nil { + data = append(data, p.decoder.ReadUInt32()) + } + } else if vr == "SL" { + for p.decoder.Len() > 0 && p.decoder.Error() == nil { + data = append(data, p.decoder.ReadInt32()) + } + } else if vr == "US" { + for p.decoder.Len() > 0 && p.decoder.Error() == nil { + data = append(data, p.decoder.ReadUInt16()) + } + } else if vr == "SS" { + for p.decoder.Len() > 0 && p.decoder.Error() == nil { + data = append(data, p.decoder.ReadInt16()) + } + } else if vr == "FL" { + for p.decoder.Len() > 0 && p.decoder.Error() == nil { + data = append(data, p.decoder.ReadFloat32()) + } + } else if vr == "FD" { + for p.decoder.Len() > 0 && p.decoder.Error() == nil { + data = append(data, p.decoder.ReadFloat64()) + } + } else { + // List of strings, each delimited by '\\'. + v := p.decoder.ReadString(int(vl)) + // String may have '\0' suffix if its length is odd. + str := strings.Trim(v, " \000") + if len(str) > 0 { + for _, s := range strings.Split(str, "\\") { + data = append(data, s) + } + } + } + } + elem.Value = data + return elem +} + +func (p *parser) DecoderError() error { + return p.decoder.Error() +} + +func (p *parser) Finish() error { + // if we've been reading from a file, close it + if p.file != nil { + p.file.Close() + } + return p.decoder.Finish() +} + +// parseFileHeader consumes the DICOM magic header and metadata elements (whose +// elements with tag group==2) from a Dicom file. Errors are reported through +// decoder.Error(). +func (p *parser) parseFileHeader() []*Element { + p.decoder.PushTransferSyntax(binary.LittleEndian, dicomio.ExplicitVR) + defer p.decoder.PopTransferSyntax() + p.decoder.Skip(128) // skip preamble + + // check for magic word + if s := p.decoder.ReadString(4); s != "DICM" { + p.decoder.SetError(errors.New("Keyword 'DICM' not found in the header")) + return nil + } + + // (0002,0000) MetaElementGroupLength + metaElem := p.ParseNext(ParseOptions{}) + if p.decoder.Error() != nil { + return nil + } + if metaElem.Tag != dicomtag.FileMetaInformationGroupLength { + p.decoder.SetErrorf("MetaElementGroupLength not found; insteadfound %s", metaElem.Tag.String()) + } + metaLength, err := metaElem.GetUInt32() + if err != nil { + p.decoder.SetErrorf("Failed to read uint32 in MetaElementGroupLength: %v", err) + return nil + } + if p.decoder.Len() <= 0 { + p.decoder.SetErrorf("No data element found") + return nil + } + metaElems := []*Element{metaElem} + + // Read meta tags + p.decoder.PushLimit(int64(metaLength)) + defer p.decoder.PopLimit() + for p.decoder.Len() > 0 { + elem := p.ParseNext(ParseOptions{}) + if p.decoder.Error() != nil { + break + } + metaElems = append(metaElems, elem) + dicomlog.Vprintf(2, "dicom.parseFileHeader: Meta elem: %v, len %v", elem.String(), p.decoder.Len()) + } + return metaElems +} + +// DataSet represents contents of one DICOM file. +type DataSet struct { + // Elements in the file, in order of appearance. + // + // Note: unlike pydicom, Elements also contains meta elements (those + // with Tag.Group==2). + Elements []*Element +} + +// FindElementByName finds an element from the dataset given the element name, +// such as "PatientName". +func (f *DataSet) FindElementByName(name string) (*Element, error) { + return FindElementByName(f.Elements, name) +} + +// FindElementByTag finds an element from the dataset given its tag, such as +// Tag{0x0010, 0x0010}. +func (f *DataSet) FindElementByTag(tag dicomtag.Tag) (*Element, error) { + return FindElementByTag(f.Elements, tag) +} + +func doassert(cond bool, values ...interface{}) { + if !cond { + var s string + for _, value := range values { + s += fmt.Sprintf("%v ", value) + } + panic(s) + } +} + +// ParseOptions defines how DataSets and Elements are parsed. +type ParseOptions struct { + // DropPixelData will cause the parser to skip the PixelData element + // (bulk images) in ReadDataSet. + DropPixelData bool + + // ReturnTags is a whitelist of tags to return. + ReturnTags []dicomtag.Tag + + // StopAtag defines a tag at which when read (or a tag with a greater + // value than it is read), the program will stop parsing the dicom file. + StopAtTag *dicomtag.Tag +} + +func getTransferSyntax(ds *DataSet) (bo binary.ByteOrder, implicit dicomio.IsImplicitVR, err error) { + elem, err := ds.FindElementByTag(dicomtag.TransferSyntaxUID) + if err != nil { + return nil, dicomio.UnknownVR, err + } + transferSyntaxUID, err := elem.GetString() + if err != nil { + return nil, dicomio.UnknownVR, err + } + return dicomio.ParseTransferSyntaxUID(transferSyntaxUID) +} + +// readNativeFrames reads Native frames from a Decoder based on already parsed pixel information +// that should be available in parsedData (elements like NumberOfFrames, rows, columns, etc) +func readNativeFrames(d *dicomio.Decoder, parsedData *DataSet) (pixelData *PixelDataInfo, bytesRead int, err error) { + image := PixelDataInfo{ + Encapsulated: false, + } + + // Parse information from previously parsed attributes that are needed to parse Native Frames: + rows, err := parsedData.FindElementByTag(dicomtag.Rows) + if err != nil { + return nil, 0, err + } + + cols, err := parsedData.FindElementByTag(dicomtag.Columns) + if err != nil { + return nil, 0, err + } + + nof, err := parsedData.FindElementByTag(dicomtag.NumberOfFrames) + nFrames := 0 + if err == nil { + // No error, so parse number of frames + nFrames, err = strconv.Atoi(nof.MustGetString()) // odd that number of frames is encoded as a string... + if err != nil { + dicomlog.Vprintf(1, "ERROR converting nof") + return nil, 0, err + } + } else { + // error fetching NumberOfFrames, so default to 1. TODO: revisit + nFrames = 1 + } + + b, err := parsedData.FindElementByTag(dicomtag.BitsAllocated) + if err != nil { + dicomlog.Vprintf(1, "ERROR finding bits allocated.") + return nil, 0, err + } + bitsAllocated := int(b.MustGetUInt16()) + image.BitsPerSample = bitsAllocated + + s, err := parsedData.FindElementByTag(dicomtag.SamplesPerPixel) + if err != nil { + dicomlog.Vprintf(1, "ERROR finding samples per pixel") + } + samplesPerPixel := int(s.MustGetUInt16()) + + pixelsPerFrame := int(rows.MustGetUInt16()) * int(cols.MustGetUInt16()) + + dicomlog.Vprintf(1, "Image size: %decoder x %decoder", rows.MustGetUInt16(), cols.MustGetUInt16()) + dicomlog.Vprintf(1, "Pixels Per Frame: %decoder", pixelsPerFrame) + dicomlog.Vprintf(1, "Number of frames %decoder", nFrames) + + // Parse the pixels: + image.NativeFrames = make([][][]int, nFrames) + for frame := 0; frame < nFrames; frame++ { + currentFrame := make([][]int, pixelsPerFrame) + for pixel := 0; pixel < int(pixelsPerFrame); pixel++ { + currentPixel := make([]int, samplesPerPixel) + for value := 0; value < samplesPerPixel; value++ { + if bitsAllocated == 8 { + currentPixel[value] = int(d.ReadUInt8()) + } else if bitsAllocated == 16 { + currentPixel[value] = int(d.ReadUInt16()) + } + } + currentFrame[pixel] = currentPixel + } + image.NativeFrames[frame] = currentFrame + } + + bytesRead = (bitsAllocated / 8) * samplesPerPixel * pixelsPerFrame * nFrames + + return &image, bytesRead, nil +} diff --git a/dicom_test.go b/parse_test.go similarity index 83% rename from dicom_test.go rename to parse_test.go index d03b61d..06f6bdf 100644 --- a/dicom_test.go +++ b/parse_test.go @@ -12,8 +12,12 @@ import ( "github.com/stretchr/testify/require" ) -func mustReadFile(path string, options dicom.ReadOptions) *dicom.DataSet { - data, err := dicom.ReadDataSetFromFile(path, options) +func mustReadFile(path string, options dicom.ParseOptions) *dicom.DataSet { + p, err := dicom.NewParserFromFile(path, nil) + if err != nil { + log.Panic(err) + } + data, err := p.Parse(options) if err != nil { log.Panic(err) } @@ -27,12 +31,12 @@ func TestAllFiles(t *testing.T) { require.NoError(t, err) for _, name := range names { t.Logf("Reading %s", name) - _ = mustReadFile("examples/"+name, dicom.ReadOptions{}) + _ = mustReadFile("examples/"+name, dicom.ParseOptions{}) } } func testWriteFile(t *testing.T, dcmPath, transferSyntaxUID string) { - data := mustReadFile(dcmPath, dicom.ReadOptions{}) + data := mustReadFile(dcmPath, dicom.ParseOptions{}) dstPath := "/tmp/test.dcm" out, err := os.Create(dstPath) require.NoError(t, err) @@ -47,7 +51,7 @@ func testWriteFile(t *testing.T, dcmPath, transferSyntaxUID string) { } err = dicom.WriteDataSet(out, data) require.NoError(t, err) - data2 := mustReadFile(dstPath, dicom.ReadOptions{}) + data2 := mustReadFile(dstPath, dicom.ParseOptions{}) if len(data.Elements) != len(data2.Elements) { t.Errorf("Wrong # of elements: %v %v", len(data.Elements), len(data2.Elements)) @@ -84,7 +88,7 @@ func TestWriteFile(t *testing.T) { } func TestReadDataSet(t *testing.T) { - data := mustReadFile("examples/IM-0001-0001.dcm", dicom.ReadOptions{}) + data := mustReadFile("examples/IM-0001-0001.dcm", dicom.ParseOptions{}) elem, err := data.FindElementByName("PatientName") require.NoError(t, err) assert.Equal(t, elem.MustGetString(), "TOUTATIX") @@ -99,14 +103,14 @@ func TestReadDataSet(t *testing.T) { // Test ReadOptions func TestReadOptions(t *testing.T) { // Test Drop Pixel Data - data := mustReadFile("examples/IM-0001-0001.dcm", dicom.ReadOptions{DropPixelData: true}) + data := mustReadFile("examples/IM-0001-0001.dcm", dicom.ParseOptions{DropPixelData: true}) _, err := data.FindElementByTag(dicomtag.PatientName) require.NoError(t, err) _, err = data.FindElementByTag(dicomtag.PixelData) require.Error(t, err) // Test Return Tags - data = mustReadFile("examples/IM-0001-0001.dcm", dicom.ReadOptions{DropPixelData: true, ReturnTags: []dicomtag.Tag{dicomtag.StudyInstanceUID}}) + data = mustReadFile("examples/IM-0001-0001.dcm", dicom.ParseOptions{DropPixelData: true, ReturnTags: []dicomtag.Tag{dicomtag.StudyInstanceUID}}) _, err = data.FindElementByTag(dicomtag.StudyInstanceUID) if err != nil { t.Error(err) @@ -118,7 +122,7 @@ func TestReadOptions(t *testing.T) { // Test Stop at Tag data = mustReadFile("examples/IM-0001-0001.dcm", - dicom.ReadOptions{ + dicom.ParseOptions{ DropPixelData: true, // Study Instance UID Element tag is Tag{0x0020, 0x000D} StopAtTag: &dicomtag.StudyInstanceUID}) @@ -134,6 +138,6 @@ func TestReadOptions(t *testing.T) { func BenchmarkParseSingle(b *testing.B) { for i := 0; i < b.N; i++ { - _ = mustReadFile("examples/IM-0001-0001.dcm", dicom.ReadOptions{}) + _ = mustReadFile("examples/IM-0001-0001.dcm", dicom.ParseOptions{}) } } diff --git a/pydicomtest/print_elements.go b/pydicomtest/print_elements.go index 4a7c326..9dcc06d 100644 --- a/pydicomtest/print_elements.go +++ b/pydicomtest/print_elements.go @@ -57,7 +57,11 @@ func main() { if err != nil { panic(err) } - data, err := dicom.ReadDataSetInBytes(bytes, dicom.ReadOptions{}) + p, err := dicom.NewParserFromBytes(bytes, nil) + if err != nil { + panic(err) + } + data, err := p.Parse(dicom.ParseOptions{}) if err != nil { panic(err) } diff --git a/query_test.go b/query_test.go index c18932d..10134cf 100644 --- a/query_test.go +++ b/query_test.go @@ -9,7 +9,11 @@ import ( ) func TestParse0(t *testing.T) { - ds, err := dicom.ReadDataSetFromFile("examples/I_000013.dcm", dicom.ReadOptions{}) + p, err := dicom.NewParserFromFile("examples/I_000013.dcm", nil) + if err != nil { + t.Fatal(err) + } + ds, err := p.Parse(dicom.ParseOptions{}) if err != nil { t.Fatal(err) } diff --git a/parser_test.go b/writer_test.go similarity index 68% rename from parser_test.go rename to writer_test.go index c263151..1a76905 100644 --- a/parser_test.go +++ b/writer_test.go @@ -1,11 +1,10 @@ -package dicom_test +package dicom import ( "encoding/binary" "reflect" "testing" - "github.com/gradienthealth/go-dicom" "github.com/gradienthealth/go-dicom/dicomio" "github.com/gradienthealth/go-dicom/dicomtag" "github.com/gradienthealth/go-dicom/dicomuid" @@ -18,33 +17,36 @@ func testWriteDataElement(t *testing.T, bo binary.ByteOrder, implicit dicomio.Is e := dicomio.NewBytesEncoder(bo, implicit) var values []interface{} values = append(values, string("FooHah")) - dicom.WriteElement(e, &dicom.Element{ + WriteElement(e, &Element{ Tag: dicomtag.Tag{0x0018, 0x9755}, Value: values}) values = nil values = append(values, uint32(1234)) values = append(values, uint32(2345)) - dicom.WriteElement(e, &dicom.Element{ + WriteElement(e, &Element{ Tag: dicomtag.Tag{0x0020, 0x9057}, Value: values}) data := e.Bytes() // Read them back. d := dicomio.NewBytesDecoder(data, bo, implicit) - elem0 := dicom.ReadElement(d, nil, dicom.ReadOptions{}) + p := parser{ + decoder: d, + } + elem0 := p.ParseNext(ParseOptions{}) - require.NoError(t, d.Error()) + require.NoError(t, p.DecoderError()) tag := dicomtag.Tag{0x18, 0x9755} assert.Equal(t, elem0.Tag, tag) assert.Equal(t, len(elem0.Value), 1) assert.Equal(t, elem0.Value[0].(string), "FooHah") tag = dicomtag.Tag{Group: 0x20, Element: 0x9057} - elem1 := dicom.ReadElement(d, nil, dicom.ReadOptions{}) - require.NoError(t, d.Error()) + elem1 := p.ParseNext(ParseOptions{}) + require.NoError(t, p.DecoderError()) assert.Equal(t, elem1.Tag, tag) assert.Equal(t, len(elem1.Value), 2) assert.Equal(t, elem1.Value[0].(uint32), uint32(1234)) assert.Equal(t, elem1.Value[1].(uint32), uint32(2345)) - require.NoError(t, d.Finish()) + require.NoError(t, p.Finish()) } func TestWriteDataElementImplicit(t *testing.T) { @@ -61,36 +63,39 @@ func TestWriteDataElementBigEndianExplicit(t *testing.T) { func TestReadWriteFileHeader(t *testing.T) { e := dicomio.NewBytesEncoder(binary.LittleEndian, dicomio.ImplicitVR) - dicom.WriteFileHeader( + WriteFileHeader( e, - []*dicom.Element{ - dicom.MustNewElement(dicomtag.TransferSyntaxUID, dicomuid.ImplicitVRLittleEndian), - dicom.MustNewElement(dicomtag.MediaStorageSOPClassUID, "1.2.840.10008.5.1.4.1.1.1.2"), - dicom.MustNewElement(dicomtag.MediaStorageSOPInstanceUID, "1.2.3.4.5.6.7"), + []*Element{ + MustNewElement(dicomtag.TransferSyntaxUID, dicomuid.ImplicitVRLittleEndian), + MustNewElement(dicomtag.MediaStorageSOPClassUID, "1.2.840.10008.5.1.4.1.1.1.2"), + MustNewElement(dicomtag.MediaStorageSOPInstanceUID, "1.2.3.4.5.6.7"), }) bytes := e.Bytes() d := dicomio.NewBytesDecoder(bytes, binary.LittleEndian, dicomio.ImplicitVR) - elems := dicom.ParseFileHeader(d) + p := parser{ + decoder: d, + } + elems := p.parseFileHeader() require.NoError(t, d.Finish()) - elem, err := dicom.FindElementByTag(elems, dicomtag.TransferSyntaxUID) + elem, err := FindElementByTag(elems, dicomtag.TransferSyntaxUID) require.NoError(t, err) assert.Equalf(t, elem.MustGetString(), dicomuid.ImplicitVRLittleEndian, "Wrong element value %+v", elem) - elem, err = dicom.FindElementByTag(elems, dicomtag.MediaStorageSOPClassUID) + elem, err = FindElementByTag(elems, dicomtag.MediaStorageSOPClassUID) require.NoError(t, err) assert.Equal(t, elem.MustGetString(), "1.2.840.10008.5.1.4.1.1.1.2") - elem, err = dicom.FindElementByTag(elems, dicomtag.MediaStorageSOPInstanceUID) + elem, err = FindElementByTag(elems, dicomtag.MediaStorageSOPInstanceUID) require.NoError(t, err) assert.Equal(t, elem.MustGetString(), "1.2.3.4.5.6.7") } func TestNewElement(t *testing.T) { - elem, err := dicom.NewElement(dicomtag.TriggerSamplePosition, uint32(10), uint32(11)) + elem, err := NewElement(dicomtag.TriggerSamplePosition, uint32(10), uint32(11)) require.NoError(t, err) require.Equal(t, elem.Tag, dicomtag.TriggerSamplePosition) require.Truef(t, reflect.DeepEqual(elem.MustGetUint32s(), []uint32{10, 11}), "Elem: %+v", elem) // Pass a wrong value type. - elem, err = dicom.NewElement(dicomtag.TriggerSamplePosition, "foo") - require.Error(t, err) + elem, err = NewElement(dicomtag.TriggerSamplePosition, "foo") + require.Error(t, err) }