{-# LANGUAGE NamedFieldPuns, OverloadedStrings #-} import Control.Applicative import Control.Monad import qualified Data.ByteString as BS import qualified Data.ByteString.Internal as BSI import qualified Data.Vector.Storable as S import Foreign.Ptr (castPtr) import Foreign.Marshal.Utils (copyBytes) import Test.Hspec import Test.HUnit import Graphics.Netpbm checkSinglePPM :: PPMType -> (Int, Int) -> PpmParseResult -> Assertion checkSinglePPM typ size parseResult = case parseResult of Right ([PPM { ppmHeader = PPMHeader { ppmType, ppmWidth, ppmHeight } }], rest) -> (ppmType, (ppmWidth, ppmHeight), rest) `shouldBe` (typ, size, Nothing) Right (ppms, _) -> assertFailure $ "expected only one image, but got " ++ show (length ppms) Left e -> assertFailure $ "image parse failed: " ++ e checkSinglePPMdata :: PPMType -> (Int, Int) -> [Int] -> PpmParseResult -> Assertion checkSinglePPMdata typ size expected parseResult = case parseResult of Right ([PPM { ppmHeader = PPMHeader { ppmType, ppmWidth, ppmHeight } , ppmData }], rest) -> do (ppmType, (ppmWidth, ppmHeight), rest) `shouldBe` (typ, size, Nothing) pixelDataToIntList ppmData `shouldBe` expected Right (ppms, _) -> assertFailure $ "expected only one image, but got " ++ show (length ppms) Left e -> assertFailure $ "image parse failed: " ++ e shouldNotParse :: PpmParseResult -> Assertion shouldNotParse res = case res of Left _ -> return () Right r -> assertFailure $ "should not parse, but parses as: " ++ show r parse :: FilePath -> IO PpmParseResult parse f = parsePPM <$> BS.readFile f parseTestFile :: String -> String -> (PpmParseResult -> Assertion) -> Spec parseTestFile name desc check = it (desc ++ " (" ++ name ++ ")") $ parse ("test/ppms/" ++ name) >>= check repcat :: Int -> [a] -> [a] repcat n = concat . replicate n -- @dir@ must have trailing slash. checkDirectory :: FilePath -> String -> PPMType -> [(String, (Int, Int))] -> Spec checkDirectory dir desc typ filesWithSizes = forM_ filesWithSizes $ \(f, size) -> parseTestFile (dir ++ f) desc $ checkSinglePPM typ size main :: IO () main = hspec $ do describe "P6 PPM (color binary)" $ do parseTestFile "gimp.ppm" "a file produced by GIMP" $ checkSinglePPM P6 (640,400) parseTestFile "gitlogo.ppm" "a file produced by convert" $ checkSinglePPM P6 (220,92) parseTestFile "image.ppm" "some random file from the internet" $ checkSinglePPM P6 (1200,1200) parseTestFile "testimg.ppm" "the color file from the netpbm test suite" $ checkSinglePPM P6 (227,149) describe "more test files from the internet" $ do checkDirectory "internet/set1/" "from the internet" P6 [ ("boxes_1.ppm", (63,63)) , ("boxes_2.ppm", (63,63)) , ("house_1.ppm", (111,132)) , ("house_2.ppm", (111,132)) , ("moreboxes_1.ppm", (63,63)) , ("moreboxes_2.ppm", (63,63)) , ("sign_1.ppm", (99,99)) , ("sign_2.ppm", (99,99)) , ("stop_1.ppm", (99,99)) , ("stop_2.ppm", (99,99)) , ("synth_1.ppm", (100,100)) , ("synth_2.ppm", (100,100)) , ("tree_1.ppm", (133,133)) , ("tree_2.ppm", (133,133)) , ("west_1.ppm", (366,216)) , ("west_2.ppm", (366,216)) ] checkDirectory "internet/set3/" "from the internet, PNM" P6 [ ("birch.pnm", (128,128)) , ("cotton.pnm", (256,170)) , ("oak.pnm", (128,128)) , ("quilt.pnm", (256,237)) ] parseTestFile "internet/set2/mandrill.ppm" "the color file from the 'Math 625' course" $ checkSinglePPM P6 (512,512) parseTestFile "internet/set2/half.ppm" "the color file from the 'Math 625' course, half width" $ checkSinglePPM P6 (256,512) parseTestFile "SIPI.ppm" "SIPI test file" $ -- convert SIPI.tiff SIPI.ppm checkSinglePPM P6 (256,256) parseTestFile "gitlogo-double.ppm" "a multi-image file" $ do \res -> case res of Right ([ PPM { ppmHeader = h1 } , PPM { ppmHeader = h2 }], rest) -> do h1 `shouldBe` PPMHeader P6 220 92 h2 `shouldBe` PPMHeader P6 220 92 rest `shouldBe` Nothing Right r -> assertFailure $ "parsed unexpected: " ++ show r Left e -> assertFailure $ "did not parse: " ++ e describe "comments" $ do parseTestFile "gitlogo-comments.ppm" "comments as a sane user would write them" $ checkSinglePPM P6 (220,92) parseTestFile "gitlogo-comment-after-magic-number.ppm" "a comment directly after the P6" $ checkSinglePPM P6 (220,92) parseTestFile "gitlogo-only-spaces-in-header.ppm" "only spaces as header separators" $ checkSinglePPM P6 (220,92) parseTestFile "gitlogo-comment-is-data.ppm" "the user thinks they wrote a comment, but it's actually parsed as data" $ \res -> case res of Right ([PPM { ppmHeader }], Just rest) -> do ppmHeader `shouldBe` PPMHeader P6 220 92 rest `shouldBe` "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\n" Right r -> assertFailure $ "parsed unexpected: " ++ show r Left e -> assertFailure $ "did not parse: " ++ e describe "weird files that are still OK with the spec" $ do parseTestFile "weird/gitlogo-width-0.ppm" "width '00' set in an image" $ \res -> case res of Right ([PPM { ppmHeader }], Just rest) -> do ppmHeader `shouldBe` PPMHeader P6 220 0 assertBool "missing rest" (BS.length rest > 200) Right r -> assertFailure $ "parsed unexpected: " ++ show r Left e -> assertFailure $ "did not parse: " ++ e parseTestFile "weird/gitlogo-comments-everywhere.ppm" "comments inside numbers" $ checkSinglePPM P6 (220,92) describe "partially valid files of which we parse as much as we can" $ do parseTestFile "graceful/face.ppm" "a PPM with a trailing newline" $ checkSinglePPM P6 (512,512) parseTestFile "graceful/gitlogo-one-and-a-half.ppm" "a multi-image file where the second image is chopped off" $ \res -> case res of Right ([PPM { ppmHeader }], Just rest) -> do ppmHeader `shouldBe` PPMHeader P6 220 92 assertBool "missing rest" (BS.length rest > 200) Right r -> assertFailure $ "parsed unexpected: " ++ show r Left e -> assertFailure $ "did not parse: " ++ e parseTestFile "graceful/gitlogo-double-with-whitespace-in-between.ppm" "a multi-image file with whitespace between the images" $ \res -> case res of Right ([ PPM { ppmHeader = h1 } , PPM { ppmHeader = h2 }], rest) -> do h1 `shouldBe` PPMHeader P6 220 92 h2 `shouldBe` PPMHeader P6 220 92 rest `shouldBe` Nothing Right r -> assertFailure $ "parsed unexpected: " ++ show r Left e -> assertFailure $ "did not parse: " ++ e describe "16-bit images" $ do -- See http://wiki.simg.de/doku.php?id=common:formats#pnm_family parseTestFile "gitlogo-16bit-created-by-simg_convert_-16be_gitlogo.ppm_output.ppm" "16-bit image created by simg" $ \res -> case res of Right ([PPM { ppmHeader, ppmData }], rest) -> do ppmHeader `shouldBe` PPMHeader P6 220 92 rest `shouldBe` Nothing case ppmData of PpmPixelDataRGB16 _ -> return () _ -> assertFailure $ "did not get 16-bit data" Right r -> assertFailure $ "parsed unexpected: " ++ show r Left e -> assertFailure $ "did not parse: " ++ e parseTestFile "SIPI-16.ppm" "SIPI test file" $ -- convert SIPI.tiff -depth 16 SIPI-16.ppm checkSinglePPM P6 (256,256) -- TODO try to get a 16-bit image out of the new gimp describe "negative examples" $ do parseTestFile "bad/gitlogo-garbage-in-numbers.ppm" "ascii characters in a number" shouldNotParse parseTestFile "bad/gitlogo-width--1.ppm" "width '-1' set in an image" shouldNotParse parseTestFile "bad/gitlogo-not-enough-data.ppm" "not containing (width * height) bytes" shouldNotParse parseTestFile "bad/gitlogo-comment-in-magic-number.ppm" "comment inside magic number" shouldNotParse parseTestFile "bad/gitlogo-comment-user-error.ppm" "a comment accidentally being put to close to a number, eating the following whitespace" $ shouldNotParse parseTestFile "bad/gitlogo-comment-user-error-no-space-after-magic.ppm" "a comment accidentally being put to close to the magic number, eating the following whitespace" $ shouldNotParse parseTestFile "bad/gitlogo-comment-without-following-extra-newline-before-data-block.ppm" "no non-comment whitespace before data block" shouldNotParse parseTestFile "bad/gitlogo-value-bigger-than-maxval.ppm" "subpixel value is bigger than maxval" shouldNotParse parseTestFile "internet/set3/cathedral.pnm" "subpixel value is bigger than maxval" shouldNotParse parseTestFile "internet/set3/checkers.pnm" "subpixel value is bigger than maxval" shouldNotParse parseTestFile "internet/set3/fish_tile.pnm" "subpixel value is bigger than maxval" shouldNotParse parseTestFile "internet/set3/garnet.pnm" "subpixel value is bigger than maxval" shouldNotParse describe "P5 PGM (greyscale binary)" $ do parseTestFile "internet/set2/mandrill.pgm" "the color file from the 'Math 625' course" $ checkSinglePPM P5 (512,512) parseTestFile "internet/set2/half.pgm" "the color file from the 'Math 625' course, half width" $ checkSinglePPM P5 (256,512) parseTestFile "SIPI-convert.pgm" "a file produced by convert" $ -- convert SIPI.tiff SIPI-convert.pgm checkSinglePPM P5 (256,256) describe "comments" $ do parseTestFile "internet/set2/comments.pgm" "the color file from the 'Math 625' course, with comments" $ checkSinglePPM P5 (512,512) describe "16-bit" $ do parseTestFile "SIPI-convert-16.pgm" "a file produced by convert, 16-bit" $ -- convert SIPI.tiff -depth 16 SIPI-convert-16.pgm checkSinglePPM P5 (256,256) describe "P4 PBM (bitmap binary)" $ do parseTestFile "testgrid.pbm" "the bitmap file from the netpbm test suite" $ checkSinglePPMdata P4 (14,16) (repcat 8 (repcat 7 [0,1] ++ replicate 14 0)) parseTestFile "SIPI-convert.pbm" "a file produced by convert" $ -- convert SIPI.tiff SIPI-convert.pbm checkSinglePPM P4 (256,256) describe "P3 PPM (color ASCII)" $ do checkDirectory "internet/set3/" "more test files from the internet" P3 [ ("feep.ppm", (4,4)) , ("snail.ppm", (256,256)) ] parseTestFile "SIPI-convert-plain.ppm" "a file produced by convert" $ -- convert SIPI.tiff -compress none SIPI-convert-plain.ppm checkSinglePPM P3 (256,256) parseTestFile "SIPI-convert-plain-16.ppm" "a file produced by convert, 16-bit" $ -- convert SIPI.tiff -compress none -depth 16 SIPI-convert-plain-16.ppm checkSinglePPM P3 (256,256) describe "P2 PGM (greyscale ASCII)" $ do checkDirectory "internet/set3/" "more test files from the internet" P2 [ ("balloons.pgm", (640,480)) , ("columns.pgm", (640,480)) , ("feep.pgm", (24,7)) , ("tracks.pgm", (300,200)) ] parseTestFile "pgm-plain-made-up-from-pbm-spec.pgm" "the plain PBM file from the spec example, converted to PGM" $ -- Invert 0/1 because in PBM 1 is black, not so in PGM checkSinglePPMdata P2 (24,7) (map (1 -) pbmFromSpecResult) parseTestFile "SIPI-convert-plain.pgm" "a file produced by convert" $ -- convert SIPI.tiff -compress none SIPI-convert-plain.pgm checkSinglePPM P2 (256,256) describe "16-bit" $ do parseTestFile "SIPI-convert-plain-16.pgm" "a file produced by convert, 16-bit" $ -- convert SIPI.tiff -compress none -depth 16 SIPI-convert-plain-16.pgm checkSinglePPM P2 (256,256) describe "P1 PBM (bitmap ASCII)" $ do describe "more test files from the internet" $ do checkDirectory "internet/set3/" "from the internet" P1 [ ("circle_ascii.pbm", (200,200)) , ("feep.pbm", (24,7)) ] parseTestFile "pbm-plain-from-spec.pbm" "the plain PBM file from the spec example" $ checkSinglePPMdata P1 (24,7) pbmFromSpecResult describe "ASCII files should only contain one image" $ do parseTestFile "pbm-plain-from-spec-multiple-but-treated-as-junk.pbm" "ASCII PBM from spec, multiple times, rest should be treated as junk" $ checkSinglePPMdata P1 (24,7) pbmFromSpecResult parseTestFile "bad/pbm-plain-from-spec-multiple-no-space-before-junk.pbm" "ASCII PBM from spec, multiple times, rest should be treated as junk" $ shouldNotParse parseTestFile "SIPI-convert-plain.pbm" "a file produced by convert" $ -- convert SIPI.tiff -compress none SIPI-convert-plain.pbm checkSinglePPM P1 (256,256) describe "In-memory format" $ do it "represents 8-bit images in rgbrgbrgb... format" $ do let d = PpmPixelDataRGB8 $ S.fromList [ PpmPixelRGB8 1 2 3 , PpmPixelRGB8 4 5 6 , PpmPixelRGB8 7 8 9 ] let PpmPixelDataRGB8 vec = d bs <- BSI.create 9 $ \bsPtrWord8 -> S.unsafeWith vec $ \ppmPixelRgb8Ptr -> copyBytes bsPtrWord8 (castPtr ppmPixelRgb8Ptr) 9 bs `shouldBe` "\1\2\3\4\5\6\7\8\9" -- Some result data -- Note that in a PBM file, "1" means black, but in the result 0 means black. pbmFromSpecResult :: [Int] pbmFromSpecResult = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 ,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1 ,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1 ,1,0,0,0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0,0,0,0,1 ,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1 ,1,0,1,1,1,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,1,1,1,1 ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]