packages feed

telescope-0.2.0: src/Telescope/Fits/Encoding.hs

module Telescope.Fits.Encoding
  ( -- * Decoding
    decode

    -- * Encoding
  , encode
  , encodePrimaryHDU
  , encodeImageHDU
  , encodeExtension
  , encodeHDU
  , replaceKeywordLine

    -- * Parser
  , nextParser
  , parseFits
  , parseMainData
  , HDUError (..)
  )
where

import Control.Exception (Exception)
import Control.Monad.Catch (MonadThrow, throwM)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Fits qualified as Fits
import Data.Fits.MegaParser qualified as Fits
import Data.Fits.Read (FitsError (..))
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Effectful
import Effectful.Error.Static
import Effectful.State.Static.Local
import Telescope.Fits.Checksum
import Telescope.Fits.DataArray (dataArray)
import Telescope.Fits.Encoding.Render
import Telescope.Fits.Types
import Text.Megaparsec qualified as M
import Text.Megaparsec.State qualified as M


{- | Decode a FITS file read as a strict 'ByteString'

>  decode =<< BS.readFile "samples/simple2x3.fits"
-}
decode :: forall m. (MonadThrow m) => ByteString -> m Fits
decode inp = do
  let res = runPureEff $ runErrorNoCallStack @HDUError $ evalState inp parseFits
  case res of
    Left e -> throwM e
    Right a -> pure a


{- | Encode a FITS file to a strict 'ByteString'

> BS.writeFile $ encdoe fits
-}
encode :: Fits -> ByteString
encode f =
  let primary = encodePrimaryHDU f.primaryHDU
      exts = fmap encodeExtension f.extensions
   in mconcat $ primary : exts


encodePrimaryHDU :: PrimaryHDU -> ByteString
encodePrimaryHDU p =
  encodeHDU (renderPrimaryHeader p.header p.dataArray) p.dataArray.rawData


encodeImageHDU :: ImageHDU -> 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


parseFits :: forall es. (State ByteString :> es, Error HDUError :> es) => Eff es Fits
parseFits = do
  p <- primary
  es <- extensions
  pure $ Fits p es
 where
  primary :: Eff es PrimaryHDU
  primary = do
    (dm, hd) <- nextParser "Primary Header" $ do
      dm <- Fits.parsePrimaryKeywords
      hd <- Fits.parseHeader
      pure (dm, hd)

    darr <- parseMainData dm
    pure $ PrimaryHDU hd darr

  extensions :: Eff es [Extension]
  extensions = do
    inp <- get @ByteString
    case inp of
      "" -> pure []
      _ -> do
        e <- extension
        es <- extensions
        pure (e : es)

  extension :: Eff es Extension
  extension = do
    -- this consumes input!
    resImg <- tryError @HDUError image
    resTbl <- tryError @HDUError 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 (_, be)) -> throwM be

  image :: Eff es ImageHDU
  image = do
    (dm, hd) <- imageHeader
    darr <- parseMainData dm
    pure $ ImageHDU hd darr

  imageHeader :: Eff es (Fits.Dimensions, Header)
  imageHeader = do
    nextParser "Image Header" $ do
      dm <- Fits.parseImageKeywords
      hd <- Fits.parseHeader
      pure (dm, hd)

  binTable :: Eff es BinTableHDU
  binTable = do
    (dm, pcount, hd) <- binTableHeader
    darr <- parseMainData dm
    rest <- get
    let heap = BS.take pcount rest
    put $ BS.dropWhile (== 0) $ BS.drop pcount rest
    pure $ BinTableHDU hd pcount heap darr

  binTableHeader :: Eff es (Fits.Dimensions, Int, Header)
  binTableHeader = do
    nextParser "BinTable Header" $ do
      (dm, pcount) <- Fits.parseBinTableKeywords
      hd <- Fits.parseHeader
      pure (dm, pcount, hd)


parseMainData :: (State ByteString :> es) => Fits.Dimensions -> Eff es DataArray
parseMainData dm = do
  rest <- get
  let len = Fits.dataSize dm
  let dat = dataArray dm (BS.take len rest)
  put $ BS.dropWhile (== 0) $ BS.drop len rest
  pure dat


-- | Parse HDUs by running MegaParsec parsers one at a time, tracking how much of the ByteString we've consumed
nextParser :: (Error HDUError :> es, State ByteString :> es) => String -> Fits.Parser a -> Eff es a
nextParser src parse = do
  bs <- get
  let st1 = M.initialState src bs
  case M.runParser' parse st1 of
    (st2, Right a) -> do
      -- only consumes input if it succeeds
      put $ BS.drop st2.stateOffset bs
      pure a
    (_, Left err) -> throwError $ FormatError $ ParseError err


data HDUError
  = InvalidExtension String
  | MissingPrimary
  | FormatError FitsError
  | InvalidHDU [FitsError]
  deriving (Show, Exception)