hsexif 0.1.0.1 → 0.2.0.0
raw patch · 3 files changed
+297/−115 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Graphics.HsExif: ApertureValue :: ExifTag
- Graphics.HsExif: BrightnessValue :: ExifTag
- Graphics.HsExif: ColorSpace :: ExifTag
- Graphics.HsExif: ComponentConfiguration :: ExifTag
- Graphics.HsExif: CompressedBitsPerPixel :: ExifTag
- Graphics.HsExif: DateTimeDigitized :: ExifTag
- Graphics.HsExif: DateTimeOriginal :: ExifTag
- Graphics.HsExif: ExifImageHeight :: ExifTag
- Graphics.HsExif: ExifImageWidth :: ExifTag
- Graphics.HsExif: ExifInteroperabilityOffset :: ExifTag
- Graphics.HsExif: ExifVersion :: ExifTag
- Graphics.HsExif: ExposureBiasValue :: ExifTag
- Graphics.HsExif: ExposureProgram :: ExifTag
- Graphics.HsExif: ExposureTime :: ExifTag
- Graphics.HsExif: FNumber :: ExifTag
- Graphics.HsExif: FileSource :: ExifTag
- Graphics.HsExif: Flash :: ExifTag
- Graphics.HsExif: FlashPixVersion :: ExifTag
- Graphics.HsExif: FocalLength :: ExifTag
- Graphics.HsExif: FocalPlaneResolutionUnit :: ExifTag
- Graphics.HsExif: FocalPlaneXResolution :: ExifTag
- Graphics.HsExif: FocalPlaneYResolution :: ExifTag
- Graphics.HsExif: ISOSpeedRatings :: ExifTag
- Graphics.HsExif: LightSource :: ExifTag
- Graphics.HsExif: MakerNote :: ExifTag
- Graphics.HsExif: MaxApertureValue :: ExifTag
- Graphics.HsExif: MeteringMode :: ExifTag
- Graphics.HsExif: RelatedSoundFile :: ExifTag
- Graphics.HsExif: SceneType :: ExifTag
- Graphics.HsExif: SensingMethod :: ExifTag
- Graphics.HsExif: ShutterSpeedValue :: ExifTag
- Graphics.HsExif: SubjectDistance :: ExifTag
- Graphics.HsExif: Unknown :: Word16 -> ExifTag
- Graphics.HsExif: UserComment :: ExifTag
+ Graphics.HsExif: ExifNumber :: Int -> ExifValue
+ Graphics.HsExif: ExifRational :: Int -> Int -> ExifValue
+ Graphics.HsExif: ExifSubIFD :: TagLocation
+ Graphics.HsExif: ExifTag :: TagLocation -> Maybe String -> Word16 -> ExifTag
+ Graphics.HsExif: ExifText :: String -> ExifValue
+ Graphics.HsExif: ExifUnknown :: Word16 -> Int -> ExifValue
+ Graphics.HsExif: HundredAndEighty :: RotationDirection
+ Graphics.HsExif: IFD0 :: TagLocation
+ Graphics.HsExif: MinusNinety :: RotationDirection
+ Graphics.HsExif: Mirror :: ImageOrientation
+ Graphics.HsExif: MirrorRotation :: RotationDirection -> ImageOrientation
+ Graphics.HsExif: Ninety :: RotationDirection
+ Graphics.HsExif: Normal :: ImageOrientation
+ Graphics.HsExif: Rotation :: RotationDirection -> ImageOrientation
+ Graphics.HsExif: apertureValue :: ExifTag
+ Graphics.HsExif: brightnessValue :: ExifTag
+ Graphics.HsExif: colorSpace :: ExifTag
+ Graphics.HsExif: componentConfiguration :: ExifTag
+ Graphics.HsExif: compressedBitsPerPixel :: ExifTag
+ Graphics.HsExif: copyright :: ExifTag
+ Graphics.HsExif: data ExifValue
+ Graphics.HsExif: data ImageOrientation
+ Graphics.HsExif: data RotationDirection
+ Graphics.HsExif: data TagLocation
+ Graphics.HsExif: dateTimeDigitized :: ExifTag
+ Graphics.HsExif: dateTimeOriginal :: ExifTag
+ Graphics.HsExif: exifImageHeight :: ExifTag
+ Graphics.HsExif: exifImageWidth :: ExifTag
+ Graphics.HsExif: exifVersion :: ExifTag
+ Graphics.HsExif: exposureBiasValue :: ExifTag
+ Graphics.HsExif: exposureProgram :: ExifTag
+ Graphics.HsExif: exposureTime :: ExifTag
+ Graphics.HsExif: fileSource :: ExifTag
+ Graphics.HsExif: flash :: ExifTag
+ Graphics.HsExif: fnumber :: ExifTag
+ Graphics.HsExif: focalLength :: ExifTag
+ Graphics.HsExif: focalPlaneResolutionUnit :: ExifTag
+ Graphics.HsExif: focalPlaneXResolution :: ExifTag
+ Graphics.HsExif: focalPlaneYResolution :: ExifTag
+ Graphics.HsExif: getOrientation :: Map ExifTag ExifValue -> Maybe ImageOrientation
+ Graphics.HsExif: instance Eq ExifValue
+ Graphics.HsExif: instance Eq ImageOrientation
+ Graphics.HsExif: instance Eq RotationDirection
+ Graphics.HsExif: instance Eq TagLocation
+ Graphics.HsExif: instance Ord TagLocation
+ Graphics.HsExif: instance Show ExifValue
+ Graphics.HsExif: instance Show ImageOrientation
+ Graphics.HsExif: instance Show RotationDirection
+ Graphics.HsExif: instance Show TagLocation
+ Graphics.HsExif: isoSpeedRatings :: ExifTag
+ Graphics.HsExif: lightSource :: ExifTag
+ Graphics.HsExif: make :: ExifTag
+ Graphics.HsExif: makerNote :: ExifTag
+ Graphics.HsExif: maxApertureValue :: ExifTag
+ Graphics.HsExif: meteringMode :: ExifTag
+ Graphics.HsExif: model :: ExifTag
+ Graphics.HsExif: orientation :: ExifTag
+ Graphics.HsExif: relatedSoundFile :: ExifTag
+ Graphics.HsExif: sceneType :: ExifTag
+ Graphics.HsExif: sensingMethod :: ExifTag
+ Graphics.HsExif: shutterSpeedValue :: ExifTag
+ Graphics.HsExif: software :: ExifTag
+ Graphics.HsExif: subjectDistance :: ExifTag
+ Graphics.HsExif: tagDesc :: ExifTag -> Maybe String
+ Graphics.HsExif: tagKey :: ExifTag -> Word16
+ Graphics.HsExif: tagLocation :: ExifTag -> TagLocation
+ Graphics.HsExif: userComment :: ExifTag
- Graphics.HsExif: getDateTimeOriginal :: Map ExifTag String -> Maybe LocalTime
+ Graphics.HsExif: getDateTimeOriginal :: Map ExifTag ExifValue -> Maybe LocalTime
- Graphics.HsExif: parseExif :: ByteString -> Either String (Map ExifTag String)
+ Graphics.HsExif: parseExif :: ByteString -> Either String (Map ExifTag ExifValue)
- Graphics.HsExif: parseFileExif :: FilePath -> IO (Either String (Map ExifTag String))
+ Graphics.HsExif: parseFileExif :: FilePath -> IO (Either String (Map ExifTag ExifValue))
Files
- Graphics/HsExif.hs +229/−109
- hsexif.cabal +1/−1
- tests/Tests.hs +67/−5
Graphics/HsExif.hs view
@@ -1,39 +1,115 @@+{-# OPTIONS_GHC -fno-warn-missing-signatures #-} -- | Ability to work with the EXIF data contained in JPEG files.-module Graphics.HsExif (ExifTag(..), parseFileExif, parseExif, getDateTimeOriginal) where+module Graphics.HsExif (+ ExifTag(..),+ TagLocation(..),+ ExifValue(..),+ parseFileExif,+ parseExif,+ getDateTimeOriginal,+ getOrientation,+ ImageOrientation(..),+ RotationDirection(..), + -- now all known exif tags+ exposureTime,+ fnumber,+ exposureProgram,+ isoSpeedRatings,+ exifVersion,+ dateTimeOriginal,+ dateTimeDigitized,+ componentConfiguration,+ compressedBitsPerPixel,+ shutterSpeedValue,+ apertureValue,+ brightnessValue,+ exposureBiasValue,+ maxApertureValue,+ subjectDistance,+ meteringMode,+ lightSource,+ flash,+ focalLength,+ makerNote,+ userComment,+ colorSpace,+ exifImageWidth,+ exifImageHeight,+ relatedSoundFile,+ focalPlaneXResolution,+ focalPlaneYResolution,+ focalPlaneResolutionUnit,+ sensingMethod,+ fileSource,+ sceneType,+ orientation,+ make,+ model,+ software,+ copyright,+) where+ import Data.Binary.Get import qualified Data.ByteString.Lazy as B import Control.Monad (liftM, unless) import qualified Data.ByteString.Char8 as Char8 import Data.Word-import Data.Int+import Data.Int (Int32) import Data.List import Data.Maybe (fromMaybe) import qualified Data.Map as Map import Data.Map (Map) import Data.Time.LocalTime import Data.Time.Calendar+import Numeric (showHex) +-- | An exif value. Exif values can also be float,+-- but this library doesn't support it yet. If you+-- have JPG files containing float exif values,+-- please send it!+--+-- If you want a string describing the contents+-- of the value, simply use 'show'.+data ExifValue = ExifNumber Int+ -- ^ An exif number. Could be short, int, signed or not.+ | ExifText String+ -- ^ ASCII text.+ | ExifRational Int Int+ -- ^ A rational number (numerator, denominator).+ -- Sometimes we're used to it as rational (exposition time: 1/160),+ -- sometimes as float (exposition compensation, we rather think -0.75)+ -- 'show' will display it as 1/160.+ | ExifUnknown Word16 Int -- type then value+ -- ^ Unknown exif tag. Not a rarity, we don't support all existing tags.+ deriving Eq++instance Show ExifValue where+ show (ExifNumber v) = show v+ show (ExifText v) = v+ show (ExifRational n d) = show n ++ "/" ++ show d+ show (ExifUnknown t v) = show "Unknown exif type. Type: " ++ show t ++ " value: " ++ show v+ -- see http://www.media.mit.edu/pia/Research/deepview/exif.html -- | Read EXIF data from the file you give. It's a key-value map.-parseFileExif :: FilePath -> IO (Either String (Map ExifTag String))+parseFileExif :: FilePath -> IO (Either String (Map ExifTag ExifValue)) parseFileExif filename = liftM parseExif $ B.readFile filename -- | Read EXIF data from a lazy bytestring.-parseExif :: B.ByteString -> Either String (Map ExifTag String)+parseExif :: B.ByteString -> Either String (Map ExifTag ExifValue) parseExif contents = case runGetOrFail getExif contents of Left (_,_,errorMsg) -> Left errorMsg Right (_,_,result) -> Right result -getExif :: Get (Map ExifTag String)+getExif :: Get (Map ExifTag ExifValue) getExif = do header <- getWord16be unless (header == 0xffd8) $ fail "Not a JPEG file" findAndParseExifBlock -findAndParseExifBlock :: Get (Map ExifTag String)+findAndParseExifBlock :: Get (Map ExifTag ExifValue) findAndParseExifBlock = do markerNumber <- getWord16be dataSize <- liftM (fromIntegral . toInteger) getWord16be@@ -54,7 +130,7 @@ getWord32 Intel = getWord32le getWord32 Motorola = getWord32be -parseExifBlock :: Get (Map ExifTag String)+parseExifBlock :: Get (Map ExifTag ExifValue) parseExifBlock = do header <- getByteString 4 nul <- liftM toInteger getWord16be@@ -62,11 +138,13 @@ $ fail "invalid EXIF header" tiffHeaderStart <- liftM fromIntegral bytesRead byteAlign <- parseTiffHeader- exifSubIfdOffset <- liftM (fromIntegral . toInteger) (parseIfd byteAlign)+ (exifSubIfdOffsetW, ifdEntries) <- parseIfd byteAlign tiffHeaderStart+ let exifSubIfdOffset = fromIntegral $ toInteger exifSubIfdOffsetW -- skip to the exif offset bytesReadNow <- liftM fromIntegral bytesRead skip $ (exifSubIfdOffset + tiffHeaderStart) - bytesReadNow- parseExifSubIfd byteAlign tiffHeaderStart+ exifSubEntries <- parseExifSubIfd byteAlign tiffHeaderStart+ return $ Map.fromList $ ifdEntries ++ exifSubEntries parseTiffHeader :: Get ByteAlign parseTiffHeader = do@@ -82,21 +160,21 @@ skip $ ifdOffset - 8 return byteAlign -parseIfd :: ByteAlign -> Get Word32-parseIfd byteAlign = do+parseIfd :: ByteAlign -> Int -> Get (Word32, [(ExifTag, ExifValue)])+parseIfd byteAlign tiffHeaderStart = do dirEntriesCount <- liftM toInteger (getWord16 byteAlign) ifdEntries <- mapM (\_ -> parseIfEntry byteAlign) [1..dirEntriesCount] let exifOffsetEntry = fromMaybe (error "Can't find the exif offset in the IFD") (find (\ e -> entryTag e == 0x8769) ifdEntries) let exifOffset = entryContents exifOffsetEntry- return exifOffset+ entries <- mapM (decodeEntry byteAlign tiffHeaderStart IFD0) ifdEntries+ return (exifOffset, entries) -parseExifSubIfd :: ByteAlign -> Int -> Get (Map ExifTag String)+parseExifSubIfd :: ByteAlign -> Int -> Get [(ExifTag, ExifValue)] parseExifSubIfd byteAlign tiffHeaderStart = do dirEntriesCount <- liftM toInteger (getWord16 byteAlign) ifdEntries <- mapM (\_ -> parseIfEntry byteAlign) [1..dirEntriesCount]- list <- mapM (decodeEntry byteAlign tiffHeaderStart) ifdEntries- return $ Map.fromList list+ mapM (decodeEntry byteAlign tiffHeaderStart ExifSubIFD) ifdEntries data IfEntry = IfEntry {@@ -120,126 +198,168 @@ entryContents = value } -data ExifTag = ExposureTime- | FNumber- | ExposureProgram- | ISOSpeedRatings- | ExifVersion- | DateTimeOriginal- | DateTimeDigitized- | ComponentConfiguration- | CompressedBitsPerPixel- | ShutterSpeedValue- | ApertureValue- | BrightnessValue- | ExposureBiasValue- | MaxApertureValue- | SubjectDistance- | MeteringMode- | LightSource- | Flash- | FocalLength- | MakerNote- | UserComment- | FlashPixVersion- | ColorSpace- | ExifImageWidth- | ExifImageHeight- | RelatedSoundFile- | ExifInteroperabilityOffset- | FocalPlaneXResolution- | FocalPlaneYResolution- | FocalPlaneResolutionUnit- | SensingMethod- | FileSource- | SceneType- | Unknown Word16- deriving (Eq, Ord, Show)+-- | Location of the tag in the JPG file structure.+-- Normally you don't need to fiddle with this,+-- except maybe if the library doesn't know the particular+-- exif tag you're interested in.+-- Rather check the list of supported exif tags, like+-- 'exposureTime' and so on.+data TagLocation = ExifSubIFD | IFD0+ deriving (Show, Eq, Ord) -getExifTag :: Word16 -> ExifTag-getExifTag entryTagV = case entryTagV of- 0x829a -> ExposureTime- 0x829d -> FNumber- 0x8822 -> ExposureProgram- 0x8827 -> ISOSpeedRatings- 0x9000 -> ExifVersion- 0x9003 -> DateTimeOriginal- 0x9004 -> DateTimeDigitized- 0x9101 -> ComponentConfiguration- 0x9102 -> CompressedBitsPerPixel- 0x9201 -> ShutterSpeedValue- 0x9202 -> ApertureValue- 0x9203 -> BrightnessValue- 0x9204 -> ExposureBiasValue- 0x9205 -> MaxApertureValue- 0x9206 -> SubjectDistance- 0x9207 -> MeteringMode- 0x9208 -> LightSource- 0x9209 -> Flash- 0x920a -> FocalLength- 0x927c -> MakerNote- 0x9286 -> UserComment- 0xa000 -> FlashPixVersion- 0xa001 -> ColorSpace- 0xa002 -> ExifImageWidth- 0xa003 -> ExifImageHeight- 0xa004 -> RelatedSoundFile- 0xa005 -> ExifInteroperabilityOffset- 0xa20e -> FocalPlaneXResolution- 0xa20f -> FocalPlaneYResolution- 0xa210 -> FocalPlaneResolutionUnit- 0xa217 -> SensingMethod- 0xa300 -> FileSource- 0xa301 -> SceneType- _ -> Unknown entryTagV+-- | An exif tag. Normally you don't need to fiddle with this,+-- except maybe if the library doesn't know the particular+-- exif tag you're interested in.+-- Rather check the list of supported exif tags, like+-- 'exposureTime' and so on.+data ExifTag = ExifTag+ {+ tagLocation :: TagLocation,+ -- ^ In which part of the JPEG file the exif tag was found+ tagDesc :: Maybe String,+ -- ^ Description of the exif tag (exposureTime, fnumber...) or if unknown, Nothing.+ -- This should be purely for debugging purposes, to compare tags use == on ExifTag+ -- or compare the tagKey.+ tagKey :: Word16+ -- ^ Exif tag key, the number uniquely identifying this tag.+ }+ deriving (Eq, Ord) -decodeEntry :: ByteAlign -> Int -> IfEntry -> Get (ExifTag, String)-decodeEntry byteAlign tiffHeaderStart entry = do- let tagKey = getExifTag $ entryTag entry+instance Show ExifTag where+ show (ExifTag _ (Just d) _) = d+ show (ExifTag l _ v) = "Unknown tag, location: " ++ show l+ ++ ", value: 0x" ++ showHex v ""++exifSubIfdTag :: String -> Word16 -> ExifTag+exifSubIfdTag d = ExifTag ExifSubIFD (Just d)++exifIfd0Tag :: String -> Word16 -> ExifTag+exifIfd0Tag d = ExifTag IFD0 (Just d)++exposureTime = exifSubIfdTag "exposureTime" 0x829a+fnumber = exifSubIfdTag "fnumber" 0x829d+exposureProgram = exifSubIfdTag "exposureProgram" 0x8822+isoSpeedRatings = exifSubIfdTag "isoSpeedRatings" 0x8827 +exifVersion = exifSubIfdTag "exifVersion" 0x9000 +dateTimeOriginal = exifSubIfdTag "dateTimeOriginal" 0x9003 +dateTimeDigitized = exifSubIfdTag "dateTimeDigitized" 0x9004 +componentConfiguration = exifSubIfdTag "componentConfiguration" 0x9101 +compressedBitsPerPixel = exifSubIfdTag "compressedBitsPerPixel" 0x9102 +shutterSpeedValue = exifSubIfdTag "shutterSpeedValue" 0x9201 +apertureValue = exifSubIfdTag "apertureValue" 0x9202 +brightnessValue = exifSubIfdTag "brightnessValue" 0x9203 +exposureBiasValue = exifSubIfdTag "exposureBiasValue" 0x9204 +maxApertureValue = exifSubIfdTag "maxApertureValue" 0x9205 +subjectDistance = exifSubIfdTag "subjectDistance" 0x9206 +meteringMode = exifSubIfdTag "meteringMode" 0x9207 +lightSource = exifSubIfdTag "lightSource" 0x9208 +flash = exifSubIfdTag "flash" 0x9209 +focalLength = exifSubIfdTag "focalLength" 0x920a +makerNote = exifSubIfdTag "makerNote" 0x927c +userComment = exifSubIfdTag "userComment" 0x9286 +colorSpace = exifSubIfdTag "colorSpace" 0xa001 +exifImageWidth = exifSubIfdTag "exifImageWidth" 0xa002 +exifImageHeight = exifSubIfdTag "exifImageHeight" 0xa003 +relatedSoundFile = exifSubIfdTag "relatedSoundFile" 0xa004 +focalPlaneXResolution = exifSubIfdTag "focalPlaneXResolution" 0xa20e +focalPlaneYResolution = exifSubIfdTag "focalPlaneYResolution" 0xa20f +focalPlaneResolutionUnit= exifSubIfdTag "focalPlaneResolutionUnit" 0xa210 +sensingMethod = exifSubIfdTag "sensingMethod" 0xa217 +fileSource = exifSubIfdTag "fileSource" 0xa300 +sceneType = exifSubIfdTag "sceneType" 0xa301 +orientation = exifIfd0Tag "orientation" 0x0112 +make = exifIfd0Tag "make" 0x010f +model = exifIfd0Tag "model" 0x0110 +software = exifIfd0Tag "software" 0x0131 +copyright = exifIfd0Tag "copyright" 0x8298 ++allExifTags :: [ExifTag]+allExifTags = [exposureTime, fnumber, exposureProgram, isoSpeedRatings,+ exifVersion, dateTimeOriginal, dateTimeDigitized, componentConfiguration,+ compressedBitsPerPixel, shutterSpeedValue, apertureValue, brightnessValue,+ exposureBiasValue, maxApertureValue, subjectDistance, meteringMode,+ lightSource, flash, focalLength, makerNote, userComment, colorSpace,+ exifImageWidth, exifImageHeight, relatedSoundFile, focalPlaneXResolution,+ focalPlaneYResolution, focalPlaneResolutionUnit, sensingMethod, fileSource,+ sceneType, orientation, make, model, software, copyright]++getExifTag :: TagLocation -> Word16 -> ExifTag+getExifTag l v = fromMaybe (ExifTag l Nothing v) $ find (isSameTag l v) allExifTags+ where isSameTag l1 v1 (ExifTag l2 _ v2) = l1 == l2 && v1 == v2++decodeEntry :: ByteAlign -> Int -> TagLocation -> IfEntry -> Get (ExifTag, ExifValue)+decodeEntry byteAlign tiffHeaderStart location entry = do+ let exifTag = getExifTag location $ entryTag entry let contentsInt = fromIntegral $ toInteger $ entryContents entry let componentsInt = fromIntegral $ toInteger $ entryNoComponents entry -- because I only know how to skip ahead, I hope the entries -- are always sorted in order of the offsets to their values... -- (maybe lookAhead could help here?) tagValue <- case entryFormat entry of- 1 -> return $ show contentsInt -- unsigned byte+ 1 -> return $ ExifNumber contentsInt -- unsigned byte 2 -> do -- ascii string curPos <- liftM fromIntegral bytesRead skip $ contentsInt + tiffHeaderStart - curPos- liftM Char8.unpack (getByteString (componentsInt-1))- 3 -> return $ show contentsInt -- unsigned short- 4 -> return $ show contentsInt -- unsigned long+ liftM (ExifText . Char8.unpack) (getByteString (componentsInt-1))+ 3 -> return $ ExifNumber contentsInt -- unsigned short+ 4 -> return $ ExifNumber contentsInt -- unsigned long 5 -> do -- unsigned rational curPos <- liftM fromIntegral bytesRead skip $ contentsInt + tiffHeaderStart - curPos- numerator <- getWord32 byteAlign- denominator <- getWord32 byteAlign- return $ show numerator ++ "/" ++ show denominator- 6 -> return $ show $ word32toint32 $ entryContents entry -- signed byte- 7 -> return $ show contentsInt -- undefined- 8 -> return $ show $ word32toint32 $ entryContents entry -- signed short- 9 -> return $ show $ word32toint32 $ entryContents entry -- signed long+ numerator <- liftM fromIntegral $ getWord32 byteAlign+ denominator <- liftM fromIntegral $ getWord32 byteAlign+ return $ ExifRational numerator denominator+ 6 -> return $ ExifNumber $ signedInt32ToInt $ entryContents entry -- signed byte+ 7 -> return $ ExifNumber contentsInt -- undefined+ 8 -> return $ ExifNumber $ signedInt32ToInt $ entryContents entry -- signed short+ 9 -> return $ ExifNumber $ signedInt32ToInt $ entryContents entry -- signed long 10 -> do -- signed rational curPos <- liftM fromIntegral bytesRead skip $ contentsInt + tiffHeaderStart - curPos- numerator <- liftM word32toint32 (getWord32 byteAlign)- denominator <- liftM word32toint32 (getWord32 byteAlign)- return $ show numerator ++ "/" ++ show denominator+ numerator <- liftM signedInt32ToInt (getWord32 byteAlign)+ denominator <- liftM signedInt32ToInt (getWord32 byteAlign)+ return $ ExifRational numerator denominator -- TODO decode float values, 11 single float and 12 double float but I'd like tests- _ -> return $ "type: " ++ show (entryFormat entry) ++ " -> " ++ show contentsInt- return (tagKey, tagValue)+ _ -> return $ ExifUnknown (entryFormat entry) contentsInt+ return (exifTag, tagValue) -word32toint32 :: Word32 -> Int32-word32toint32 word = fromIntegral word :: Int32+signedInt32ToInt :: Word32 -> Int+signedInt32ToInt w = fromIntegral (fromIntegral w :: Int32) -- | Extract the date and time when the picture was taken -- from the EXIF information.-getDateTimeOriginal :: Map ExifTag String -> Maybe LocalTime+getDateTimeOriginal :: Map ExifTag ExifValue -> Maybe LocalTime getDateTimeOriginal exifData = do - dateStr <- Map.lookup DateTimeOriginal exifData+ dateStr <- liftM show $ Map.lookup dateTimeOriginal exifData -- i know more elegant ways to code this.. parsec, regex, text.. -- but i don't want to bring in too many dependencies to this library. -- the date is like "YYYY:MM:DD HH:MM:SS" return $ LocalTime (fromGregorian (read $ take 4 dateStr) (read $ take 2 . drop 5 $ dateStr) (read $ take 2 . drop 8 $ dateStr)) (TimeOfDay (read $ take 2 . drop 11 $ dateStr) (read $ take 2 . drop 14 $ dateStr) (read $ take 2 . drop 17 $ dateStr))++data RotationDirection = MinusNinety+ | Ninety+ | HundredAndEighty+ deriving (Show, Eq)++data ImageOrientation = Normal+ | Mirror+ | Rotation RotationDirection+ | MirrorRotation RotationDirection+ deriving (Show, Eq)++-- | Extract the image orientation from the EXIF information.+getOrientation :: Map ExifTag ExifValue -> Maybe ImageOrientation+getOrientation exifData = do+ rotationVal <- Map.lookup orientation exifData+ case rotationVal of+ ExifNumber 1 -> Just Normal+ ExifNumber 2 -> Just Mirror+ ExifNumber 3 -> Just $ Rotation HundredAndEighty+ ExifNumber 4 -> Just $ MirrorRotation HundredAndEighty+ ExifNumber 5 -> Just $ MirrorRotation MinusNinety+ ExifNumber 6 -> Just $ Rotation MinusNinety+ ExifNumber 7 -> Just $ MirrorRotation Ninety+ ExifNumber 8 -> Just $ Rotation Ninety+ _ -> error $ show rotationVal
hsexif.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: hsexif-version: 0.1.0.1+version: 0.2.0.0 synopsis: EXIF handling library in pure Haskell description: The hsexif library provides functions for working with EXIF data contained in JPEG files. Currently it only supports reading the data.
tests/Tests.hs view
@@ -4,19 +4,24 @@ import Graphics.HsExif import qualified Data.ByteString.Lazy as B import qualified Data.Map as Map+import Data.Map (Map) import Data.Time.LocalTime import Data.Time.Calendar+import Data.Word main :: IO () main = do imageContents <- B.readFile "tests/test.jpg" noExif <- B.readFile "tests/noexif.jpg" png <- B.readFile "tests/test.png"+ let parseExifCorrect = (\(Right x) -> x) . parseExif+ let exifData = parseExifCorrect imageContents hspec $ do describe "not a JPG" $ testNotAJpeg png describe "no EXIF" $ testNoExif noExif describe "basic parsing" $ testBasic imageContents- describe "extract picture date" $ testDate imageContents+ describe "extract picture date" $ testDate exifData+ describe "image orientation" $ testOrientation exifData testNotAJpeg :: B.ByteString -> Spec testNotAJpeg imageContents = it "returns empty list if not a JPEG" $@@ -28,9 +33,66 @@ testBasic :: B.ByteString -> Spec testBasic imageContents = it "parses a simple JPEG" $- assertEqual "doesn't match" (Right $ Map.fromList [(ExposureTime,"1/160"),(FNumber,"0/10"),(ExposureProgram,"2"),(ISOSpeedRatings,"1600"),(ExifVersion,"808661552"),(DateTimeOriginal,"2013:10:02 20:33:33"),(DateTimeDigitized,"2013:10:02 20:33:33"),(ComponentConfiguration,"197121"),(CompressedBitsPerPixel,"2/1"),(BrightnessValue,"-4226/2560"),(ExposureBiasValue,"0/10"),(MaxApertureValue,"0/10"),(MeteringMode,"5"),(LightSource,"0"),(Flash,"24"),(FocalLength,"0/10"),(MakerNote,"868"),(UserComment,"36568"),(FlashPixVersion,"808464688"),(ColorSpace,"1"),(ExifImageWidth,"1"),(ExifImageHeight,"1"),(ExifInteroperabilityOffset,"36640"),(FileSource,"3"),(SceneType,"1"),(Unknown 41985,"0"),(Unknown 41986,"0"),(Unknown 41987,"0"),(Unknown 41988,"16/16"),(Unknown 41989,"0"),(Unknown 41990,"0"),(Unknown 41992,"0"),(Unknown 41993,"0"),(Unknown 41994,"0")]) (parseExif imageContents)+ assertEqual "doesn't match" (Right $ Map.fromList [+ (exposureTime, ExifRational 1 160),+ (fnumber, ExifRational 0 10),+ (exposureProgram, ExifNumber 2),+ (isoSpeedRatings, ExifNumber 1600),+ (exifVersion, ExifNumber 808661552),+ (dateTimeOriginal, ExifText "2013:10:02 20:33:33"),+ (dateTimeDigitized, ExifText "2013:10:02 20:33:33"),+ (componentConfiguration, ExifNumber 197121),+ (compressedBitsPerPixel, ExifRational 2 1),+ (brightnessValue, ExifRational (-4226) 2560),+ (exposureBiasValue, ExifRational 0 10),+ (maxApertureValue, ExifRational 0 10),+ (meteringMode, ExifNumber 5),+ (lightSource, ExifNumber 0),+ (fileSource, ExifNumber 3),+ (flash, ExifNumber 24),+ (focalLength, ExifRational 0 10),+ (makerNote, ExifNumber 868),+ (userComment, ExifNumber 36568),+ (unknownExifSubIfdTag 40960, ExifNumber 808464688),+ (colorSpace, ExifNumber 1),+ (sceneType, ExifNumber 1),+ (exifImageWidth, ExifNumber 1),+ (exifImageHeight, ExifNumber 1),+ (unknownExifSubIfdTag 40965, ExifNumber 36640),+ (unknownExifSubIfdTag 41985, ExifNumber 0),+ (unknownExifSubIfdTag 41986, ExifNumber 0),+ (unknownExifSubIfdTag 41987, ExifNumber 0),+ (unknownExifSubIfdTag 41988, ExifRational 16 16),+ (unknownExifSubIfdTag 41989, ExifNumber 0),+ (unknownExifSubIfdTag 41990, ExifNumber 0),+ (unknownExifSubIfdTag 41992, ExifNumber 0),+ (unknownExifSubIfdTag 41993, ExifNumber 0),+ (unknownExifSubIfdTag 41994, ExifNumber 0),+ (make, ExifText "SONY"),+ (model, ExifText "NEX-3N"),+ (software, ExifText "GIMP 2.8.10"),+ (orientation, ExifNumber 1),+ (unknownExifIfd0Tag 0x10e, ExifText " "),+ (unknownExifIfd0Tag 0x11a, ExifRational 350 1),+ (unknownExifIfd0Tag 0x11b, ExifRational 350 1),+ (unknownExifIfd0Tag 0x128, ExifNumber 2),+ (unknownExifIfd0Tag 0x132, ExifText "2014:04:10 20:14:20"),+ (unknownExifIfd0Tag 0x213, ExifNumber 2),+ (unknownExifIfd0Tag 0x8769, ExifNumber 358),+ (unknownExifIfd0Tag 0xc4a5, ExifNumber 252)+ ]) (parseExif imageContents) -testDate :: B.ByteString -> Spec-testDate imageContents = it "extracts the date correctly" $+unknownExifSubIfdTag :: Word16 -> ExifTag+unknownExifSubIfdTag = ExifTag ExifSubIFD Nothing++unknownExifIfd0Tag :: Word16 -> ExifTag+unknownExifIfd0Tag = ExifTag IFD0 Nothing++testDate :: Map ExifTag ExifValue -> Spec+testDate exifData = it "extracts the date correctly" $ assertEqual "doesn't match" (Just $ LocalTime (fromGregorian 2013 10 2) (TimeOfDay 20 33 33))- (getDateTimeOriginal $ (\(Right x) -> x) $ parseExif imageContents)+ (getDateTimeOriginal exifData)++testOrientation :: Map ExifTag ExifValue -> Spec+testOrientation exifData = it "reads exif orientation" $+ assertEqual "doesn't match" (Just Normal) $ getOrientation exifData