aboutsummaryrefslogtreecommitdiff
path: root/ch10/10_a_1.hs
blob: 4a0c1cdc2b72eec60ce43962db8b5132a77039ad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
-- 1. Write a parser for "plain" PGM files.
--
-- 2. In our description of "raw" PGM files, we omitted a small detail. If the
--    "maximum grey" value in the header is less than 256, each pixel is
--    represented by a single byte. However, it can range up to 65,535, in which
--    case, each pixel will be represented by 2 bytes, in big-endian order (most
--    significant byte first).
--
--    Rewrite the raw PGM parser to accommodate both the single- and double-byte
--    pixel formats.
--
-- 3. Extend your parser so that it can identify a raw or plain PGM file, and parse
--    the appropriate file type.


{-- From examples/examples/ch10/Parse.hs modified according to the assignment --}
import Control.Applicative ((<$>))
import qualified Data.ByteString.Lazy as L
import Data.Char (chr, isDigit, isSpace)
import Data.Int (Int64)
import Data.Word (Word8)


data ParseState = ParseState {
      string :: L.ByteString
    , offset :: Int64
    } deriving (Show)

newtype Parse a = Parse {
      runParse :: ParseState -> Either String (a, ParseState)
    }

identity :: a -> Parse a
identity a = Parse (\s -> Right (a, s))

bail :: String -> Parse a
bail err = Parse $ \s -> Left $
           "byte offset " ++ show (offset s) ++ ": " ++ err

(==>) :: Parse a -> (a -> Parse b) -> Parse b

firstParser ==> secondParser  =  Parse chainedParser
  where chainedParser initState   =
          case runParse firstParser initState of
            Left errMessage ->
                Left errMessage
            Right (firstResult, newState) ->
                runParse (secondParser firstResult) newState


getState :: Parse ParseState
getState = Parse (\s -> Right (s, s))

putState :: ParseState -> Parse ()
putState s = Parse (\_ -> Right ((), s))

parseByte :: Parse Word8
parseByte =
    getState ==> \initState ->
    case L.uncons (string initState) of
      Nothing ->
          bail "no more input"
      Just (byte,remainder) ->
          putState newState ==> \_ ->
          identity byte
        where newState = initState { string = remainder,
                                     offset = newOffset }
              newOffset = offset initState + 1

peekByte :: Parse (Maybe Word8)
peekByte = (fmap fst . L.uncons . string) <$> getState

instance Functor Parse where
    fmap f parser = parser ==> \result ->
                    identity (f result)

peekChar :: Parse (Maybe Char)
peekChar = fmap w2c <$> peekByte

w2c :: Word8 -> Char
w2c = chr . fromIntegral

parseWhile :: (Word8 -> Bool) -> Parse [Word8]
parseWhile p = (fmap p <$> peekByte) ==> \mp ->
               if mp == Just True
               then parseByte ==> \b ->
                    (b:) <$> parseWhile p
               else identity []

parseWhileWith :: (Word8 -> a) -> (a -> Bool) -> Parse [a]
parseWhileWith f p = fmap f <$> parseWhile (p . f)

parseNat :: Parse Int
parseNat = parseWhileWith w2c isDigit ==> \digits ->
           if null digits
           then bail "no more input"
           else let n = read digits
                in if n < 0
                   then bail "integer overflow"
                   else identity n

(==>&) :: Parse a -> Parse b -> Parse b
p ==>& f = p ==> \_ -> f

skipSpaces :: Parse ()
skipSpaces = parseWhileWith w2c isSpace ==>& identity ()

assert :: Bool -> String -> Parse ()
assert True  _   = identity ()
assert False err = bail err

parseBytes :: Int -> Parse L.ByteString
parseBytes n =
    getState ==> \st ->
    let n' = fromIntegral n
        (h, t) = L.splitAt n' (string st)
        st' = st { offset = offset st + L.length h, string = t }
    in putState st' ==>&
       assert (L.length h == n') "end of input" ==>&
       identity h

parseRawPGM :: Int -> WordSize -> Parse [Int]
parseRawPGM pixelCount wordSize =
    parseByte ==>&
    parseBytes byteCount ==> \bitmap ->
    identity (wordsToInts bitmap)
  where (byteCount, wordsToInts) = if wordSize == WordSize16bit
                                   then ((2 * pixelCount), words8ToInts16)
                                   else (pixelCount, words8ToInts8)
{-- End of code from examples --}


{-- From examples/examples/ch10/Parse.hs modified according to the assignment --}
data Greymap = Greymap {
      greyWidth :: Int
    , greyHeight :: Int
    , greyMax :: Int
    , greyData :: [Int]
    } deriving (Eq)
{-- End of code from examples --}

type PGMType = String

data WordSize = WordSize8bit | WordSize16bit deriving (Eq)

instance Show Greymap where
    show (Greymap w h m d) = "Greymap " ++ show w ++ "x" ++ show h ++
                             " " ++ show m ++ " " ++ show d


words8ToInts8 :: L.ByteString -> [Int]
words8ToInts8 words =
  case (L.uncons words) of
    Nothing       -> []
    Just (w, rem) -> (fromIntegral w) : (words8ToInts8 rem)


words8ToInts16 :: L.ByteString -> [Int]
words8ToInts16 words = ints8ToInts16 (words8ToInts8 words)
  where
    ints8ToInts16 (a:b:rem) = ((a * 256) + b) : (ints8ToInts16 rem)
    ints8ToInts16 _ = []


is16Bit :: Greymap -> Bool
is16Bit (Greymap _ _ m _) = (m > 255)


parseHeaderPGM =
    parseWhileWith w2c notWhite ==> \header -> skipSpaces ==>&
    parseNat ==> \width -> skipSpaces ==>&
    parseNat ==> \height -> skipSpaces ==>&
    parseNat ==> \maxGrey ->
    identity (header, (Greymap width height maxGrey []))
  where notWhite = (`notElem` " \r\n\t")


parsePlainPixel :: Parse Int
parsePlainPixel = skipSpaces ==>& parseNat


parsePlainPGM :: Int -> Parse [Int]
parsePlainPGM pixelCount =
  if pixelCount == 0
  then
    identity []
  else
    parsePlainPixel ==> \pixelValue ->
    (pixelValue:) <$> parsePlainPGM (pixelCount - 1)


parsePGMFile :: FilePath -> IO (Either String Greymap)
parsePGMFile filePath = do
  str <- L.readFile filePath
  let state = ParseState str 0
  case (runParse parseHeaderPGM) state of
    Left err              -> return (Left err)
    Right (header, state) -> return $ parsePGMFile' header state
  where
    parsePGMFile' :: (PGMType, Greymap) -> ParseState -> Either String Greymap
    parsePGMFile' (pgmType, greymap) state
      | pgmType == "P2"                    = parse (parsePlainPGM pixelCount) state
      | pgmType == "P5" && is16Bit greymap = parse (parseRawPGM pixelCount WordSize16bit) state
      | pgmType == "P5"                    = parse (parseRawPGM pixelCount WordSize8bit) state
      | otherwise                          = Left "Unknown PGM type"
      where (Greymap width height maxGrey _) = greymap
            pixelCount = width * height
            parse parser state = case (runParse parser) state of
                                   Left err -> Left err
                                   Right (body, state) -> Right (Greymap width height maxGrey body)


-- ghci> :l 10_a_1.hs
-- [1 of 1] Compiling Main             ( 10_a_1.hs, interpreted )
-- Ok, one module loaded.

-- ghci> parsePGMFile "test-10_a_1/plain.pgm"
-- Right Greymap 3x3 255 [0,12,65,101,143,192,200,224,255]

-- ghci> parsePGMFile "test-10_a_1/raw_8bit.pgm"
-- Right Greymap 3x3 255 [0,12,65,101,143,192,200,224,255]

-- ghci> parsePGMFile "test-10_a_1/raw_16bit.pgm"
-- Right Greymap 3x3 65535 [0,3073,16642,25859,36612,49157,51206,57351,65535]