packages feed

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 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