dicom-0.1.0.0: src/Data/DICOM/Object.hs
-----------------------------------------------------------------------------
--
-- Module : Data.DICOM.Object
-- Copyright : Copyright (c) DICOM Grid 2015
-- License : GPL-3
--
-- Maintainer : paf31@cantab.net
-- Stability : experimental
-- Portability :
--
-- | Types and smart constructors for DICOM objects and elements.
--
-----------------------------------------------------------------------------
{-# LANGUAGE PatternSynonyms #-}
module Data.DICOM.Object
( ElementContent(..)
, Element(..)
, SequenceItem(..)
, Sequence(..)
, Object(..)
, readObject
, readObjectFromFile
, writeObject
, writeObjectToFile
, element
, sq
, item
, object
, ae
, as
, cs
, da
, ds
, dt
, fl
, fd
, is
, lo
, lt
, ob
, ow
, pn
, sh
, sl
, ss
, st
, tm
, ui
, ul
, un
, us
, ut
) where
import Prelude hiding (LT)
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.Bool (bool)
import Data.Int (Int64)
import Data.Monoid (Monoid(..), (<>))
import Data.Foldable (traverse_)
import Data.List (sortBy)
import Data.Function (on)
import Data.Time.Clock (UTCTime)
import Data.Time.Format (formatTime)
import System.Locale (defaultTimeLocale)
import Data.DICOM.VL
import Data.DICOM.VR
import Data.DICOM.Tag
import Control.Monad (unless)
import Control.Applicative
import Text.Printf (printf)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL
-- Magic constants
dicm :: B.ByteString
dicm = BC.pack "DICM"
-- Data types
data ElementContent
= BytesContent B.ByteString
| SequenceContent Sequence deriving (Show, Eq)
data Element = Element
{ elementTag :: Tag
, elementVL :: VL
, elementVR :: VR
, elementContent :: ElementContent
} deriving (Show, Eq)
data SequenceItem = SequenceItem
{ sequenceItemLength :: Word32
, sequenceItemElements :: [Element]
} deriving (Show, Eq)
newtype Sequence = Sequence { runSequence :: [SequenceItem] } deriving (Show, Eq)
newtype Object = Object { runObject :: [Element] } deriving (Show, Eq)
-- Serialization
instance Binary Element where
get = do
_tag <- get
_vr <- get
_vl <- if isVLReserved _vr then
(do skip 2
vl . fromIntegral <$> getWord32le)
else vl . fromIntegral <$> getWord16le
content <- case _vr of
SQ -> SequenceContent <$> readSequence _vl
_ -> case _vl of
UndefinedValueLength -> failWithOffset "Undefined VL not implemented"
_ -> do
bytes <- getByteString $ fromIntegral $ runVL _vl
return $ BytesContent bytes
return $ Element _tag _vl _vr content
put el = do
put $ elementTag el
put $ elementVR el
if isVLReserved (elementVR el) then
(do putWord16le 0
putWord32le . fromIntegral . runVL $ elementVL el)
else putWord16le . fromIntegral . runVL $ elementVL el
case elementContent el of
SequenceContent s -> writeSequence (elementVL el) s
BytesContent bs -> putByteString bs
readSequence :: VL -> Get Sequence
readSequence UndefinedValueLength = do
els <- untilG (isSequenceDelimitationItem <$> get) get
SequenceDelimitationItem <- get
skip 4
return $ Sequence els
readSequence _vl = Sequence <$> untilByteCount (fromIntegral $ runVL _vl) get
writeSequence :: VL -> Sequence -> Put
writeSequence _vl s = do
traverse_ put (runSequence s)
case _vl of
UndefinedValueLength -> do
put SequenceDelimitationItem
putWord32le 0
_ -> return ()
instance Binary SequenceItem where
get = do
t <- get
case t of
Item -> do
itemLength <- getWord32le
case vl (fromIntegral itemLength) of
UndefinedValueLength -> do
els <- untilG (isItemDelimitationItem <$> get) get
ItemDelimitationItem <- get
skip 4
return $ SequenceItem itemLength els
_ -> do
els <- untilByteCount (fromIntegral itemLength) get
return $ SequenceItem itemLength els
_ -> failWithOffset "Unexpected tag"
put si = do
put Item
putWord32le $ sequenceItemLength si
traverse_ put $ sequenceItemElements si
case vl (fromIntegral (sequenceItemLength si)) of
UndefinedValueLength -> do
put ItemDelimitationItem
putWord32le 0
_ -> return ()
isItemDelimitationItem :: Tag -> Bool
isItemDelimitationItem ItemDelimitationItem = True
isItemDelimitationItem _ = False
isSequenceDelimitationItem :: Tag -> Bool
isSequenceDelimitationItem SequenceDelimitationItem = True
isSequenceDelimitationItem _ = False
untilG :: Get Bool -> Get a -> Get [a]
untilG more a = lookAhead more >>= bool ((:) <$> a <*> untilG more a) (pure [])
untilByteCount :: Int64 -> Get a -> Get [a]
untilByteCount count a = do
start <- bytesRead
flip untilG a $ do
end <- bytesRead
return (end - start < count)
isVLReserved :: VR -> Bool
isVLReserved OB = True
isVLReserved OW = True
isVLReserved OF = True
isVLReserved SQ = True
isVLReserved UT = True
isVLReserved UN = True
isVLReserved _ = False
instance Binary Object where
get = do
skip 128
header <- getByteString 4
unless (header == dicm) $ failWithOffset "Invalid DICOM header"
Object <$> untilG isEmpty get
put obj = do
putByteString $ B.replicate 128 0
putByteString dicm
let fileMetaInfo = takeWhile ((== TagGroup 0x0002) . tagGroup . elementTag) $ runObject obj
groupLength = sum $ map (BL.length . encode) fileMetaInfo
put $ tag (TagGroup 0x0002) (TagElement 0x0000)
put UL
putWord16le 4
putWord32le $ fromIntegral groupLength
traverse_ put (runObject obj)
failWithOffset :: String -> Get a
failWithOffset msg = do
offset <- bytesRead
fail $ "Error at offset " ++ printf "%08x" offset ++ ": " ++ msg
readObject :: BL.ByteString -> Either String Object
readObject bs = case decodeOrFail bs of
Left (_ , _, e) -> Left e
Right (rest, _, _) | not (BL.null rest) -> Left "Unconsumed input"
Right (_ , _, a) -> Right a
writeObject :: Object -> BL.ByteString
writeObject = encode
-- File IO
readObjectFromFile :: FilePath -> IO (Either String Object)
readObjectFromFile path = readObject <$> BL.readFile path
writeObjectToFile :: FilePath -> Object -> IO ()
writeObjectToFile path = BL.writeFile path . writeObject
-- Smart constructors for DICOM objects
-- TODO: some of these constructors could benefit from better types
instance Monoid Object where
mempty = Object []
mappend (Object es1) (Object es2) = Object (sortBy (compare `on` elementTag) $ es1 ++ es2)
element :: VR -> Tag -> B.ByteString -> Element
element vr tg content = Element tg (vl $ fromIntegral count) vr (BytesContent padded)
where
(count, padded) = case B.length content of
len | len `mod` 2 == 0 -> (len, content)
| otherwise -> (len + 1, content <> BC.pack [padChar])
padChar | isStringVR vr = ' '
| otherwise = '\0'
-- String value representations
ae :: Tag -> String -> Element
ae t = element AE t . BC.pack
as :: Tag -> String -> Element
as t = element AS t . BC.pack
cs :: Tag -> String -> Element
cs t = element CS t . BC.pack
ds :: Tag -> B.ByteString -> Element
ds = element DS
fl :: Tag -> B.ByteString -> Element
fl = element FL
fd :: Tag -> B.ByteString -> Element
fd = element FD
is :: Tag -> Int -> Element
is t = element IS t . BC.pack . show
lo :: Tag -> String -> Element
lo t = element LO t . BC.pack
lt :: Tag -> String -> Element
lt t = element LT t . BC.pack
pn :: Tag -> String -> Element
pn t = element PN t . BC.pack
sh :: Tag -> String -> Element
sh t = element SH t . BC.pack
sl :: Tag -> B.ByteString -> Element
sl = element SL
ss :: Tag -> B.ByteString -> Element
ss = element SS
st :: Tag -> B.ByteString -> Element
st = element ST
ui :: Tag -> String -> Element
ui t = element UI t . BC.pack
ul :: Tag -> B.ByteString -> Element
ul = element UL
un :: Tag -> B.ByteString -> Element
un = element UN
us :: Tag -> B.ByteString -> Element
us = element US
ut :: Tag -> B.ByteString -> Element
ut = element UT
-- Binary value representations
ob :: Tag -> B.ByteString -> Element
ob = element OB
ow :: Tag -> B.ByteString -> Element
ow = element OW
-- Date/time value representations
da :: Tag -> UTCTime -> Element
da t = element DA t . BC.pack . formatTime defaultTimeLocale "%Y%m%d"
dt :: Tag -> UTCTime -> Element
dt t = element DT t . BC.pack . formatTime defaultTimeLocale "%Y%m%d%H%M%S.000000&0000"
tm :: Tag -> UTCTime -> Element
tm t = element TM t . BC.pack . formatTime defaultTimeLocale "%H%M%S.000000"
sq :: Tag -> [SequenceItem] -> Element
sq tg items = Element tg UndefinedValueLength SQ (SequenceContent (Sequence items))
item :: [Element] -> SequenceItem
item = SequenceItem (fromIntegral . runVL $ UndefinedValueLength) . sortBy (compare `on` elementTag)
object :: [Element] -> Object
object = Object . sortBy (compare `on` elementTag)