packages feed

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"