hsexif-0.6.0.2: tests/Tests.hs
{-# LANGUAGE OverloadedStrings #-}
import Test.Hspec
import Test.HUnit
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString as BS
import Data.Text (Text)
import qualified Data.Map as Map
import Data.Map (Map)
import Data.Time.LocalTime
import Data.Time.Calendar
import Data.Char (chr)
import Control.Monad (join)
import Control.Applicative ( (<$>) )
import Data.List
import Graphics.HsExif
import Graphics.Helpers
main :: IO ()
main = do
imageContents <- B.readFile "tests/test.jpg"
noExif <- B.readFile "tests/noexif.jpg"
png <- B.readFile "tests/test.png"
gps <- B.readFile "tests/gps.jpg"
gps2 <- B.readFile "tests/gps2.jpg"
partial <- B.readFile "tests/partial_exif.jpg"
let parseExifM = hush . parseExif
let exifData = parseExifM imageContents
let gpsExifData = parseExifM gps
let gps2ExifData = parseExifM gps2
hspec $ do
describe "not a JPG" $ testNotAJpeg png
describe "no EXIF" $ testNoExif noExif
describe "basic parsing" $ testBasic imageContents
describe "extract picture date" $ testDate exifData
describe "image orientation" $ testOrientation exifData
describe "read exif date time" testReadExifDateTime
describe "read GPS lat long" $ testReadGpsLatLong gpsExifData
describe "read GPS lat long -- no data" $ testReadGpsLatLongNoData exifData
describe "test formatAsFloatingPoint" testFormatAsFloatingPoint
describe "pretty printing" $ testPrettyPrint gpsExifData exifData gps2ExifData
describe "flash fired" $ testFlashFired exifData
describe "partial exif data" $ testPartialExif partial
testNotAJpeg :: B.ByteString -> Spec
testNotAJpeg imageContents = it "returns empty list if not a JPEG" $
assertEqual' (Left "Not a JPEG file") (parseExif imageContents)
testNoExif :: B.ByteString -> Spec
testNoExif imageContents = it "returns empty list if no EXIF" $
assertEqual' (Left "No EXIF in JPEG") (parseExif imageContents)
testBasic :: B.ByteString -> Spec
testBasic imageContents = it "parses a simple JPEG" $ do
case cleanedParsedM of
Nothing -> assertBool "Parsing fails" False
Just cleanedParsed -> assertEqualListDebug
(sort [
(exposureTime, ExifRational 1 160),
(fnumber, ExifRational 0 10),
(exposureProgram, ExifNumber 2),
(isoSpeedRatings, ExifNumber 1600),
(exifVersion, ExifUndefined "0230"),
(dateTimeOriginal, ExifText "2013:10:02 20:33:33"),
(dateTimeDigitized, ExifText "2013:10:02 20:33:33"),
(componentConfiguration, ExifUndefined "\SOH\STX\ETX\NUL"),
(compressedBitsPerPixel, ExifRational 2 1),
(brightnessValue, ExifRational (-4226) 2560),
(exposureBiasValue, ExifRational 0 10),
(maxApertureValue, ExifRational 0 10),
(meteringMode, ExifNumber 5),
(lightSource, ExifNumber 0),
(flash, ExifNumber 24),
(focalLength, ExifRational 0 10),
(flashPixVersion, ExifUndefined "0100"),
(colorSpace, ExifNumber 1),
(sceneType, ExifUndefined "\SOH"),
(exifImageWidth, ExifNumber 1),
(exifImageHeight, ExifNumber 1),
(exifInteroperabilityOffset, ExifNumber 36616),
(customRendered, ExifNumber 0),
(exposureMode, ExifNumber 0),
(whiteBalance, ExifNumber 0),
(digitalZoomRatio, ExifRational 16 16),
(focalLengthIn35mmFilm, ExifNumber 0),
(contrast, ExifNumber 0),
(saturation, ExifNumber 0),
(sceneCaptureType, ExifNumber 0),
(sharpness, ExifNumber 0),
(make, ExifText "SONY"),
(model, ExifText "NEX-3N"),
(software, ExifText "GIMP 2.8.10"),
(orientation, ExifNumber 1),
(imageDescription, ExifText " "),
(xResolution, ExifRational 350 1),
(yResolution, ExifRational 350 1),
(resolutionUnit, ExifNumber 2),
(userComment, ExifUndefined "UNICODE\NULT\NULe\NULs\NULt\NUL \NULE\NULx\NULi\NULf\NUL \NULc\NULo\NULm\NULm\NULe\NULn\NULt\NUL\r\SOHa\SOH~\SOH"),
(dateTime, ExifText "2014:04:10 20:14:20"),
(yCbCrPositioning, ExifNumber 2),
(fileSource, ExifUndefined "\ETX"),
(exifIfdOffset, ExifNumber 358),
(printImageMatching, ExifUndefined "PrintIM\NUL0300\NUL\NUL\ETX\NUL\STX\NUL\SOH\NUL\NUL\NUL\ETX\NUL\"\NUL\NUL\NUL\SOH\SOH\NUL\NUL\NUL\NUL\t\DC1\NUL\NUL\DLE'\NUL\NUL\v\SI\NUL\NUL\DLE'\NUL\NUL\151\ENQ\NUL\NUL\DLE'\NUL\NUL\176\b\NUL\NUL\DLE'\NUL\NUL\SOH\FS\NUL\NUL\DLE'\NUL\NUL^\STX\NUL\NUL\DLE'\NUL\NUL\139\NUL\NUL\NUL\DLE'\NUL\NUL\203\ETX\NUL\NUL\DLE'\NUL\NUL\229\ESC\NUL\NUL\DLE'\NUL\NUL")
]) (sort cleanedParsed)
-- the sony maker note is 35k!! Just test its size and that it starts with "SONY DSC".
assertEqual' (Just 35692) (BS.length <$> makerNoteV)
assertEqual' (Just "SONY DSC") (take 8 <$> fmap (chr . fromIntegral) <$> BS.unpack <$> makerNoteV)
where
-- the makerNote is HUGE. so test it separately.
parsed = hush $ parseExif imageContents
cleanedParsedM = filter ((/=makerNote) . fst) <$> (Map.toList <$> parsed)
makerNoteV = parsed >>= Map.lookup makerNote >>= getUndefMaybe
getUndefMaybe :: ExifValue -> Maybe BS.ByteString
getUndefMaybe (ExifUndefined x) = Just x
getUndefMaybe _ = Nothing
testDate :: Maybe (Map ExifTag ExifValue) -> Spec
testDate exifData = it "extracts the date correctly" $
assertEqual' (Just $ LocalTime (fromGregorian 2013 10 2) (TimeOfDay 20 33 33))
(exifData >>= getDateTimeOriginal)
testOrientation :: Maybe (Map ExifTag ExifValue) -> Spec
testOrientation exifData = it "reads exif orientation" $
assertEqual' (Just Normal) $ exifData >>= getOrientation
testReadExifDateTime :: Spec
testReadExifDateTime = it "reads exif date time" $ do
assertEqual' (Just $ LocalTime (fromGregorian 2013 10 2) (TimeOfDay 20 33 33)) (readExifDateTime "2013:10:02 20:33:33")
assertEqual' Nothing (readExifDateTime "2013:10:02 20:33:3")
testReadGpsLatLong :: Maybe (Map ExifTag ExifValue) -> Spec
testReadGpsLatLong exifData = it "reads gps latitude longitude" $ do
let (Just (lat,long)) = exifData >>= getGpsLatitudeLongitude
assertBool' $ 50.2179 < lat && 50.2180 > lat
assertBool' $ -5.031 > long && -5.032 < long
testReadGpsLatLongNoData :: Maybe (Map ExifTag ExifValue) -> Spec
testReadGpsLatLongNoData exifData = it "reads gps latitude longitude" $
assertEqual' Nothing $ join $ getGpsLatitudeLongitude <$> exifData
testFormatAsFloatingPoint :: Spec
testFormatAsFloatingPoint = it "properly formats as floating point" $ do
assertEqual' "0.75" $ formatAsFloatingPoint 2 $ ExifRational 3 4
assertEqual' "0.75, -0.50, 0.25" $ formatAsFloatingPoint 2 $ ExifRationalList [(3,4),(-1,2),(1,4)]
testPrettyPrint :: Maybe (Map ExifTag ExifValue)
-> Maybe (Map ExifTag ExifValue)
-> Maybe (Map ExifTag ExifValue) -> Spec
testPrettyPrint exifData stdExifData gps2ExifData = it "pretty prints many tags properly" $ do
checkPrettyPrinter orientation "Top-left" exifData
checkPrettyPrinter flash "Flash did not fire, auto mode" exifData
checkPrettyPrinter exposureTime "1/200 sec." exifData
checkPrettyPrinter maxApertureValue "f/2.0" exifData
checkPrettyPrinter exposureProgram "Normal program" exifData
checkPrettyPrinter exifVersion "Exif version 2.30" exifData
checkPrettyPrinter componentConfiguration "YCbCr-" exifData
checkPrettyPrinter exposureBiasValue "0.00 EV" exifData
checkPrettyPrinter meteringMode "Pattern" exifData
checkPrettyPrinter lightSource "Unknown" exifData
checkPrettyPrinter flashPixVersion "FlashPix version 1.0" exifData
checkPrettyPrinter colorSpace "sRGB" exifData
checkPrettyPrinter customRendered "Normal process" exifData
checkPrettyPrinter exposureMode "Auto exposure" exifData
checkPrettyPrinter whiteBalance "Auto white balance" exifData
checkPrettyPrinter sceneCaptureType "Standard" exifData
checkPrettyPrinter gainControl "Normal" exifData
checkPrettyPrinter contrast "Normal" exifData
checkPrettyPrinter saturation "Normal" exifData
checkPrettyPrinter resolutionUnit "Inch" exifData
checkPrettyPrinter yCbCrPositioning "Co-sited" exifData
checkPrettyPrinter gpsLatitude "50° 13' 4.50\"" exifData
checkPrettyPrinter gpsLatitudeRef "North" exifData
checkPrettyPrinter gpsAltitude "2681.1111" gps2ExifData
checkPrettyPrinter gpsTimeStamp "09:12:32.21" gps2ExifData
checkPrettyPrinter userComment "Test Exif comment" exifData
checkPrettyPrinter userComment "Test Exif commentčšž" stdExifData
checkPrettyPrinter :: ExifTag -> Text -> Maybe (Map ExifTag ExifValue) -> Assertion
checkPrettyPrinter tag str exifData = assertEqual' (Just str) $ prettyPrinter tag <$> (exifData >>= Map.lookup tag)
testFlashFired :: Maybe (Map ExifTag ExifValue) -> Spec
testFlashFired exifData = it "properly reads whether the flash was fired" $ do
assertEqual' (Just False) $ exifData >>= wasFlashFired
assertEqual' Nothing $ wasFlashFired Map.empty
let check (ex, val) = Just ex == wasFlashFired (makeExifMapWithFlash val)
assertBool "a number test fails" $ all check [
(False, 0), (True, 1), (True, 5), (True, 7), (True, 9), (True, 0x0D),
(True, 0x0F), (False, 0x10), (False, 0x18), (True, 0x19), (True, 0x1D),
(True, 0x1F), (False, 0x20), (True, 0x41), (True, 0x45), (True, 0x47),
(True, 0x4D), (True, 0x4F), (True, 0x59), (True, 0x5D), (True, 0x5F)]
makeExifMapWithFlash :: Int -> Map ExifTag ExifValue
makeExifMapWithFlash flashV = Map.fromList [(flash, ExifNumber flashV)]
testPartialExif :: B.ByteString -> Spec
testPartialExif imageContents = it "parses a partial exif JPEG" $
assertEqual' (Right []) (Map.toList <$> parseExif imageContents)
assertEqualListDebug :: (Show a, Eq a) => [a] -> [a] -> Assertion
assertEqualListDebug = assertEqualListDebug' (0 :: Int)
where
assertEqualListDebug' idx (x:xs) (y:ys) = do
assertEqual ("index " ++ show idx ++ " differs: " ++ show x ++ " /= " ++ show y) x y
assertEqualListDebug' (idx+1) xs ys
assertEqualListDebug' _ (x:_) [] = assertBool ("List lengths differ, expected " ++ show x) False
assertEqualListDebug' _ [] (y:_) = assertBool ("List lengths differ, got " ++ show y) False
assertEqualListDebug' _ [] [] = assertBool "" True
assertEqual' :: (Show a, Eq a) => a -> a -> Assertion
assertEqual' = assertEqual "doesn't match"
assertBool' :: Bool -> Assertion
assertBool' = assertBool "doesn't match"