Skip to content

Commit

Permalink
Merge pull request #14 from slicebox/release-0-1-1
Browse files Browse the repository at this point in the history
Moved Dicom parts to DicomParts, added some utils for attributes
  • Loading branch information
kobmic committed May 10, 2017
2 parents 28d5d2f + 1d4fb30 commit 3970ca6
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 67 deletions.
2 changes: 1 addition & 1 deletion build.sbt
@@ -1,7 +1,7 @@
import de.heikoseeberger.sbtheader.license.Apache2_0

name := "dcm4che-streams"
version := "0.2-SNAPSHOT"
version := "0.1.1"
organization := "se.nimsa"
scalaVersion := "2.12.2"
crossScalaVersions := Seq("2.11.8", "2.12.2")
Expand Down
Expand Up @@ -18,14 +18,12 @@ package se.nimsa.dcm4che.streams

import akka.stream.scaladsl.Sink
import org.dcm4che3.data.{Attributes, Fragments, Sequence}
import se.nimsa.dcm4che.streams.DicomParts._

import scala.concurrent.{ExecutionContext, Future}

object DicomAttributesSink {

import DicomPartFlow._
import DicomFlows._

private case class AttributesData(attributesStack: Seq[Attributes],
sequenceStack: Seq[Sequence],
currentFragments: Option[Fragments])
Expand Down
29 changes: 4 additions & 25 deletions src/main/scala/se/nimsa/dcm4che/streams/DicomFlows.scala
Expand Up @@ -21,24 +21,13 @@ import java.util.zip.Deflater
import akka.NotUsed
import akka.stream.scaladsl.{Flow, Source}
import akka.util.ByteString
import se.nimsa.dcm4che.streams.DicomParts._

/**
* Various flows for transforming streams of <code>DicomPart</code>s.
*/
object DicomFlows {

import DicomPartFlow._

case class DicomAttribute(header: DicomHeader, valueChunks: Seq[DicomValueChunk]) extends DicomPart {
def bytes = valueChunks.map(_.bytes).fold(ByteString.empty)(_ ++ _)

def bigEndian = header.bigEndian
}

case class DicomFragment(bigEndian: Boolean, valueChunks: Seq[DicomValueChunk]) extends DicomPart {
def bytes = valueChunks.map(_.bytes).fold(ByteString.empty)(_ ++ _)
}

case class ValidationContext(sopClassUID: String, transferSyntax: String)


Expand Down Expand Up @@ -101,7 +90,7 @@ object DicomFlows {
* @param tagsWhitelist list of tags to keep.
* @return the associated filter Flow
*/
def whitelistFilter(tagsWhitelist: Seq[Int]): Flow[DicomPartFlow.DicomPart, DicomPartFlow.DicomPart, NotUsed] = whitelistFilter(tagsWhitelist.contains(_))
def whitelistFilter(tagsWhitelist: Seq[Int]): Flow[DicomPart, DicomPart, NotUsed] = whitelistFilter(tagsWhitelist.contains(_))

/**
* Filter a stream of dicom parts such that all attributes that are group length elements except
Expand Down Expand Up @@ -131,7 +120,7 @@ object DicomFlows {
* @param keepPreamble true if preamble should be kept, else false
* @return Flow of filtered parts
*/
def whitelistFilter(tagCondition: (Int) => Boolean, keepPreamble: Boolean = false): Flow[DicomPartFlow.DicomPart, DicomPartFlow.DicomPart, NotUsed] = tagFilter(tagCondition, isWhitelist = true, keepPreamble)
def whitelistFilter(tagCondition: (Int) => Boolean, keepPreamble: Boolean = false): Flow[DicomPart, DicomPart, NotUsed] = tagFilter(tagCondition, isWhitelist = true, keepPreamble)


private def tagFilter(tagCondition: (Int) => Boolean, isWhitelist: Boolean, keepPreamble: Boolean) = Flow[DicomPart].statefulMapConcat {
Expand Down Expand Up @@ -224,16 +213,6 @@ object DicomFlows {
var headerMaybe: Option[DicomHeader] = None
var transformMaybe: Option[ByteString => ByteString] = None

// update header length according to new value
def updateHeader(header: DicomHeader, newLength: Int): DicomHeader = {
val updatedBytes =
if (header.vr.headerLength() == 8)
header.bytes.take(6) ++ DicomParsing.shortToBytes(newLength.toShort, header.bigEndian)
else
header.bytes.take(8) ++ DicomParsing.intToBytes(newLength, header.bigEndian)
header.copy(length = newLength, bytes = updatedBytes)
}

{
case header: DicomHeader if tags.contains(header.tag) =>
headerMaybe = Some(header)
Expand All @@ -244,7 +223,7 @@ object DicomFlows {
value = value ++ chunk.bytes
if (chunk.last) {
val newValue = transformMaybe.map(t => t(value)).getOrElse(value)
val newHeader = headerMaybe.map(updateHeader(_, newValue.length)).get
val newHeader = headerMaybe.map(header => header.withUpdatedLength(newValue.length.toShort)).get
transformMaybe = None
headerMaybe = None
newHeader :: DicomValueChunk(chunk.bigEndian, newValue, last = true) :: Nil
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/se/nimsa/dcm4che/streams/DicomParsing.scala
Expand Up @@ -144,7 +144,7 @@ trait DicomParsing {
}

/**
* Read header of data element for implicit VR, handles special case for FileMetaInformationVersion.
* Read header of data element for implicit VR.
*
* @param buffer current buffer
* @return
Expand Down
37 changes: 4 additions & 33 deletions src/main/scala/se/nimsa/dcm4che/streams/DicomPartFlow.scala
Expand Up @@ -24,7 +24,7 @@ import akka.util.ByteString
import org.dcm4che3.data.{ElementDictionary, VR}
import org.dcm4che3.io.DicomStreamException
import org.dcm4che3.util.TagUtils
import se.nimsa.dcm4che.streams.DicomPartFlow._
import se.nimsa.dcm4che.streams.DicomParts._

/**
* Flow which ingests a stream of bytes and outputs a stream of DICOM file parts such as specified by the <code>DicomPart</code>
Expand Down Expand Up @@ -118,7 +118,7 @@ class DicomPartFlow(chunkSize: Int = 8192, stopTag: Option[Int] = None, inflate:
case _ =>
state.copy(pos = updatedPos)
}
val part = Some(DicomHeader(tag, updatedVr, valueLength, isFmi = true, state.bigEndian, bytes))
val part = Some(DicomHeader(tag, updatedVr, valueLength, isFmi = true, state.bigEndian, state.explicitVR, bytes))
val nextStep = updatedState.fmiEndPos.filter(_ <= updatedPos) match {
case Some(_) =>
reader.ensure(valueLength + 2)
Expand All @@ -135,7 +135,7 @@ class DicomPartFlow(chunkSize: Int = 8192, stopTag: Option[Int] = None, inflate:
def parse(reader: ByteReader) = {
val attribute = readDatasetHeader(reader, state)
val nextState = attribute.map {
case DicomHeader(_, _, length, _, bigEndian, _) => InValue(ValueState(bigEndian, length, InDatasetHeader(state, inflater)))
case DicomHeader(_, _, length, _, bigEndian, _, _) => InValue(ValueState(bigEndian, length, InDatasetHeader(state, inflater)))
case DicomFragments(_, _, bigEndian, _) => InFragments(FragmentsState(bigEndian, state.explicitVR), inflater)
case _ => InDatasetHeader(state, inflater)
}.getOrElse(FinishedParser)
Expand Down Expand Up @@ -252,7 +252,7 @@ class DicomPartFlow(chunkSize: Int = 8192, stopTag: Option[Int] = None, inflate:
else if (valueLength == -1)
Some(DicomFragments(tag, vr, state.bigEndian, bytes))
else
Some(DicomHeader(tag, updatedVr2, valueLength, isFmi = false, state.bigEndian, bytes))
Some(DicomHeader(tag, updatedVr2, valueLength, isFmi = false, state.bigEndian, state.explicitVR, bytes))
} else
tag match {
case 0xFFFEE000 => Some(DicomItem(valueLength, state.bigEndian, reader.take(8)))
Expand All @@ -271,34 +271,5 @@ class DicomPartFlow(chunkSize: Int = 8192, stopTag: Option[Int] = None, inflate:

object DicomPartFlow {

trait DicomPart {
def bigEndian: Boolean
def bytes: ByteString
}

case class DicomPreamble(bytes: ByteString) extends DicomPart {
def bigEndian = false
}

case class DicomHeader(tag: Int, vr: VR, length: Int, isFmi: Boolean, bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomValueChunk(bigEndian: Boolean, bytes: ByteString, last: Boolean) extends DicomPart

case class DicomDeflatedChunk(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomItem(length: Int, bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomItemDelimitation(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomSequence(tag: Int, bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomSequenceDelimitation(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomFragments(tag: Int, vr: VR, bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomFragmentsDelimitation(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomUnknownPart(bigEndian: Boolean, bytes: ByteString) extends DicomPart

val partFlow = new DicomPartFlow()
}
106 changes: 106 additions & 0 deletions src/main/scala/se/nimsa/dcm4che/streams/DicomParts.scala
@@ -0,0 +1,106 @@
/*
* Copyright 2017 Lars Edenbrandt
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package se.nimsa.dcm4che.streams

import akka.util.ByteString
import org.dcm4che3.data.{SpecificCharacterSet, VR}


object DicomParts {

trait DicomPart {
def bigEndian: Boolean

def bytes: ByteString
}

case class DicomPreamble(bytes: ByteString) extends DicomPart {
def bigEndian = false
}

case class DicomHeader(tag: Int, vr: VR, length: Int, isFmi: Boolean, bigEndian: Boolean, explicitVR: Boolean, bytes: ByteString) extends DicomPart {

def withUpdatedLength(newLength: Int) : DicomHeader = {

val updated = if ((bytes.size >= 8) && (explicitVR) && (vr.headerLength == 8)) { //explicit vr
bytes.take(6) ++ DicomParsing.shortToBytes(newLength.toShort, bigEndian)
} else if ((bytes.size >= 12) && (explicitVR) && (vr.headerLength == 12)) { //explicit vr
bytes.take(8) ++ DicomParsing.intToBytes(newLength, bigEndian)
} else { //implicit vr
bytes.take(4) ++ DicomParsing.intToBytes(newLength, bigEndian)
}

DicomHeader(tag, vr, newLength, isFmi, bigEndian, explicitVR, updated)
}

}

case class DicomValueChunk(bigEndian: Boolean, bytes: ByteString, last: Boolean) extends DicomPart

case class DicomDeflatedChunk(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomItem(length: Int, bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomItemDelimitation(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomSequence(tag: Int, bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomSequenceDelimitation(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomFragments(tag: Int, vr: VR, bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomFragmentsDelimitation(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomUnknownPart(bigEndian: Boolean, bytes: ByteString) extends DicomPart

case class DicomFragment(bigEndian: Boolean, valueChunks: Seq[DicomValueChunk]) extends DicomPart {
def bytes = valueChunks.map(_.bytes).fold(ByteString.empty)(_ ++ _)
}

case class DicomAttribute(header: DicomHeader, valueChunks: Seq[DicomValueChunk]) extends DicomPart {
def bytes = valueChunks.map(_.bytes).fold(ByteString.empty)(_ ++ _)

def bigEndian = header.bigEndian

// LO: long string 64 chars max
// SH: short string 16 chars max
// PN: person name
// UI: 64 chars max
def withUpdatedStringValue(newValue: String, cs: SpecificCharacterSet = SpecificCharacterSet.ASCII): DicomAttribute = {
val newBytes = header.vr.toBytes(newValue, cs)
val needsPadding = newBytes.size % 2 == 1
val newLength = if (needsPadding) newBytes.size + 1 else newBytes.size
val updatedHeader = header.withUpdatedLength(newLength.toShort)
val updatedValue = if (needsPadding) {
ByteString.fromArray(newBytes :+ header.vr.paddingByte().toByte)
} else {
ByteString.fromArray(newBytes)
}
DicomAttribute(updatedHeader, Seq(DicomValueChunk(header.bigEndian, updatedValue, true)))
}

// DA: A string of characters of the format YYYYMMDD, 8 bytes fixed
def withUpdatedDateValue(newValue: String, cs: SpecificCharacterSet = SpecificCharacterSet.ASCII): DicomAttribute = {
val newBytes = header.vr.toBytes(newValue, cs)
val updatedValue = ByteString.fromArray(newBytes)
DicomAttribute(header, Seq(DicomValueChunk(header.bigEndian, updatedValue, true)))
}

def asDicomParts: Seq[DicomPart] = header +: valueChunks
}

}
1 change: 1 addition & 0 deletions src/main/scala/se/nimsa/dcm4che/streams/DicomSinks.scala
Expand Up @@ -19,6 +19,7 @@ package se.nimsa.dcm4che.streams
import akka.stream.SinkShape
import akka.stream.scaladsl.{Broadcast, GraphDSL, Sink}
import akka.util.ByteString
import se.nimsa.dcm4che.streams.DicomParts.DicomPart

import scala.concurrent.Future

Expand Down
3 changes: 1 addition & 2 deletions src/test/scala/se/nimsa/dcm4che/streams/DicomData.scala
Expand Up @@ -5,8 +5,7 @@ import java.util.zip.Deflater
import akka.stream.testkit.TestSubscriber
import akka.util.ByteString
import org.dcm4che3.data.VR
import se.nimsa.dcm4che.streams.DicomFlows.{DicomAttribute, DicomFragment}
import se.nimsa.dcm4che.streams.DicomPartFlow._
import se.nimsa.dcm4che.streams.DicomParts._

object DicomData {

Expand Down
Expand Up @@ -10,7 +10,7 @@ import akka.testkit.TestKit
import akka.util.ByteString
import org.dcm4che3.data.{Tag, VR}
import org.scalatest.{FlatSpecLike, Matchers}
import se.nimsa.dcm4che.streams.DicomPartFlow.DicomPart
import se.nimsa.dcm4che.streams.DicomParts.DicomPart


class DicomFlowsTest extends TestKit(ActorSystem("DicomAttributesSinkSpec")) with FlatSpecLike with Matchers {
Expand Down
Expand Up @@ -12,7 +12,7 @@ import org.scalatest.{FlatSpecLike, Matchers}
class DicomPartFlowTest extends TestKit(ActorSystem("DicomFlowSpec")) with FlatSpecLike with Matchers {

import DicomData._
import DicomPartFlow._
import DicomParts._

implicit val materializer = ActorMaterializer()
implicit val ec = system.dispatcher
Expand Down
65 changes: 65 additions & 0 deletions src/test/scala/se/nimsa/dcm4che/streams/DicomPartsTest.scala
@@ -0,0 +1,65 @@
package se.nimsa.dcm4che.streams

import org.scalatest.{FlatSpecLike, Matchers}
import DicomData._
import akka.util.ByteString
import se.nimsa.dcm4che.streams.DicomParts.{DicomAttribute, DicomHeader, DicomValueChunk}

class DicomPartsTest extends FlatSpecLike with Matchers {

"DicomHeader" should "should return a new header with modified length for explicitVR, LE" in {
val (tag, vr, headerLength, length) = DicomParsing.readHeaderExplicitVR(patientNameJohnDoe, false).get
val header = DicomHeader(tag, vr, length, false, false, true, patientNameJohnDoe.take(8))
val updatedHeader = header.withUpdatedLength(5)

updatedHeader.length shouldEqual 5
updatedHeader.bytes.take(6) shouldEqual header.bytes.take(6)
updatedHeader.bytes(6) shouldEqual 5
updatedHeader.bytes(7) shouldEqual 0
}

it should "should return a new header with modified length for explicitVR, BE" in {
val (tag, vr, headerLength, length) = DicomParsing.readHeaderExplicitVR(patientNameJohnDoeBE, true).get
val header = DicomHeader(tag, vr, length, false, true, true, patientNameJohnDoeBE.take(8))
val updatedHeader = header.withUpdatedLength(5)

updatedHeader.length shouldEqual 5
updatedHeader.bytes.take(6) shouldEqual header.bytes.take(6)
updatedHeader.bytes(6) shouldEqual 0
updatedHeader.bytes(7) shouldEqual 5
}

it should "should return a new header with modified length for implicitVR, LE" in {
val (tag, vr, headerLength, length) = DicomParsing.readHeaderImplicitVR(patientNameJohnDoeImplicit).get
val header = DicomHeader(tag, vr, length, false, false, false, patientNameJohnDoeImplicit.take(8))
val updatedHeader = header.withUpdatedLength(5)

updatedHeader.length shouldEqual 5
updatedHeader.bytes.take(4) shouldEqual header.bytes.take(4)
updatedHeader.bytes(4) shouldEqual 5
updatedHeader.bytes(5) shouldEqual 0
}


"DicomAttribute" should "should return a new attribute with updated header and updated value" in {
val (tag, vr, headerLength, length) = DicomParsing.readHeaderExplicitVR(patientNameJohnDoe, false).get
val header = DicomHeader(tag, vr, length, false, false, true, patientNameJohnDoe.take(8))
val value = DicomValueChunk(false, patientNameJohnDoe.drop(8) ,true)
val attribute = DicomAttribute(header, Seq(value))
val updatedAttribute = attribute.withUpdatedStringValue("Jimmyboy^Doe")
updatedAttribute.bytes.size shouldEqual 12
updatedAttribute.header.length shouldEqual 12
}

it should "should return a new attribute with updated header and updated value with padding" in {
val (tag, vr, headerLength, length) = DicomParsing.readHeaderExplicitVR(patientNameJohnDoe, false).get
val header = DicomHeader(tag, vr, length, false, false, true, patientNameJohnDoe.take(8))
val value = DicomValueChunk(false, patientNameJohnDoe.drop(8) ,true)
val attribute = DicomAttribute(header, Seq(value))
val updatedAttribute = attribute.withUpdatedStringValue("Jimmy^Doe")

updatedAttribute.bytes.size shouldEqual 10
updatedAttribute.header.length shouldEqual 10
updatedAttribute.bytes.drop(9) shouldEqual ByteString(32)
}
}

0 comments on commit 3970ca6

Please sign in to comment.