telescope-0.3.0: src/Telescope/Fits/Encoding.hs
module Telescope.Fits.Encoding where
import Control.Monad.Catch (MonadThrow, throwM)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.State.Static.Local
import Telescope.Data.Parser
import Telescope.Fits.Checksum
import Telescope.Fits.DataArray (DataArray (..), Dimensions (..), dataArray, dataSizeBytes)
import Telescope.Fits.Encoding.MegaHeader qualified as MH
import Telescope.Fits.Encoding.Render
import Telescope.Fits.HDU
import Telescope.Fits.Header
import Text.Megaparsec (lookAhead)
import Text.Megaparsec qualified as MP
{- | Decode a FITS file read from a strict 'ByteString'
> decode =<< BS.readFile "samples/simple2x3.fits"
-}
decode :: forall m. (MonadThrow m) => ByteString -> m Fits
decode = fitsParseThrow parseFits
{- | Encode a FITS file to a strict 'ByteString'
> BS.writeFile $ encode fits
-}
encode :: Fits -> ByteString
encode f =
let prim = encodePrimaryHDU f.primaryHDU
exts = fmap encodeExtension f.extensions
in mconcat $ prim : exts
fitsParseThrow :: (MonadThrow m) => Eff '[State ByteString, Parser] a -> ByteString -> m a
fitsParseThrow parse inp =
case runFitsParse parse inp of
Left e -> throwM e
Right a -> pure a
runFitsParse :: Eff '[State ByteString, Parser] a -> BS.ByteString -> Either ParseError a
runFitsParse parse inp = do
runPureEff $ runParser $ evalState inp parse
parseFits :: forall es. (State ByteString :> es, Parser :> es) => Eff es Fits
parseFits = do
p <- primary
es <- parseAt (Child "extensions") $ extensions 1
pure $ Fits p es
primary :: (State ByteString :> es, Parser :> es) => Eff es DataHDU
primary = do
parseAt (Child "primary") $ do
(dm, hd) <- runMega "Primary Header" $ do
dm <- MP.lookAhead MH.parsePrimaryKeywords
hd <- MH.parseHeader
pure (dm, hd)
darr <- mainData dm
pure $ DataHDU hd darr
extensions :: (State ByteString :> es, Parser :> es) => Int -> Eff es [Extension]
extensions n = do
inp <- get @ByteString
-- recurse until you run out of input or error
case inp of
"" -> pure []
_ -> do
e <- parseAt (Index n) extension
es <- extensions (n + 1)
pure (e : es)
extension :: (State ByteString :> es, Parser :> es) => Eff es Extension
extension = do
resImg <- runParser image
resTbl <- runParser binTable
case (resImg, resTbl) of
(Right i, _) -> pure $ Image i
(_, Right b) -> pure $ BinTable b
-- (Left (_, FormatError ie), Left (_, FormatError be)) -> throwM $ InvalidHDU [ie, be]
(Left _, Left (ParseFailure p e)) -> do
send $ PathMod (<> p) $ parseFail e
image :: (State ByteString :> es, Parser :> es) => Eff es DataHDU
image = do
(dm, hd) <- runMega "Image Header" $ do
dm <- MP.lookAhead MH.parseImageKeywords
hd <- MH.parseHeader
pure (dm, hd)
darr <- mainData dm
pure $ DataHDU hd darr
binTable :: (State ByteString :> es, Parser :> es) => Eff es BinTableHDU
binTable = do
(dm, pcount, hd) <- do
runMega "BinTable Header" $ do
(dm, pcount) <- lookAhead MH.parseBinTableKeywords
hd <- MH.parseHeader
pure (dm, pcount, hd)
darr <- mainData dm
rest <- get
let heap = BS.take pcount rest
put $ BS.dropWhile (== 0) $ BS.drop pcount rest
pure $ BinTableHDU hd pcount heap darr
mainData :: (State ByteString :> es) => Dimensions -> Eff es DataArray
mainData dm = do
rest <- get
let len = dataSizeBytes dm
let dat = dataArray dm (BS.take len rest)
put $ BS.dropWhile (== 0) $ BS.drop len rest
pure dat
encodePrimaryHDU :: DataHDU -> ByteString
encodePrimaryHDU p =
encodeHDU (renderPrimaryHeader p.header p.dataArray) p.dataArray.rawData
encodeImageHDU :: DataHDU -> ByteString
encodeImageHDU p =
encodeHDU (renderImageHeader p.header p.dataArray) p.dataArray.rawData
encodeExtension :: Extension -> ByteString
encodeExtension (Image hdu) = encodeImageHDU hdu
encodeExtension (BinTable _) = error "BinTableHDU rendering not supported"
-- | Encode an HDU, properly handling datasum and checksum
encodeHDU :: (Checksum -> BuilderBlock) -> ByteString -> ByteString
encodeHDU buildHead rawData =
let dsum = checksum rawData
in encodeHeader dsum <> renderDataArray rawData
where
encodeHeader :: Checksum -> ByteString
encodeHeader dsum =
let h = BS.toStrict $ runRender (buildHead dsum)
hsum = checksum h -- calculate the checksum of only the header
csum = hsum <> dsum -- 1s complement add to the datasum
in replaceChecksum csum h
replaceChecksum :: Checksum -> ByteString -> ByteString
replaceChecksum csum = replaceKeywordLine "CHECKSUM" (String $ encodeChecksum csum) Nothing
-- | Fast replace a single keyword in a raw header bytestring
replaceKeywordLine :: ByteString -> Value -> Maybe Text -> ByteString -> ByteString
replaceKeywordLine key val mc header =
let (start, rest) = BS.breakSubstring key header
newKeyLine = BS.toStrict $ runRender $ renderKeywordLine (TE.decodeUtf8 key) val mc
in start <> newKeyLine <> BS.drop 80 rest
-- | Parse HDUs by running MegaParsec parsers one at a time, and tracking how much of the ByteString we've consumed
runMega :: (Parser :> es, State ByteString :> es) => String -> MH.Parser a -> Eff es a
runMega src parse = do
inp <- get
case MH.runNextParser src inp parse of
Right (a, rest) -> do
put rest
pure a
Left err ->
parseFail $ MH.showParseError err