packages feed

hsexif-0.1.0.1: Graphics/HsExif.hs

-- | Ability to work with the EXIF data contained in JPEG files.
module Graphics.HsExif (ExifTag(..), parseFileExif, parseExif, getDateTimeOriginal) 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.List
import Data.Maybe (fromMaybe)
import qualified Data.Map as Map
import Data.Map (Map)
import Data.Time.LocalTime
import Data.Time.Calendar

-- 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 filename = liftM parseExif $ B.readFile filename

-- | Read EXIF data from a lazy bytestring.
parseExif :: B.ByteString -> Either String (Map ExifTag String)
parseExif contents = case runGetOrFail getExif contents of
		Left (_,_,errorMsg) -> Left errorMsg
		Right (_,_,result) -> Right result

getExif :: Get (Map ExifTag String)
getExif = do
	header <- getWord16be
	unless (header == 0xffd8)
		$ fail "Not a JPEG file"
	findAndParseExifBlock

findAndParseExifBlock :: Get (Map ExifTag String)
findAndParseExifBlock = do
	markerNumber <- getWord16be
	dataSize <- liftM (fromIntegral . toInteger) getWord16be
	case markerNumber of
		0xffe1 -> parseExifBlock
		-- ffda is Start Of Stream => image
		-- I expect no more EXIF data after this point.
		0xffda -> fail "No EXIF in JPEG" 
		_ -> skip (dataSize-2) >> findAndParseExifBlock

data ByteAlign = Intel | Motorola

getWord16 :: ByteAlign -> Get Word16
getWord16 Intel = getWord16le
getWord16 Motorola = getWord16be

getWord32 :: ByteAlign -> Get Word32
getWord32 Intel = getWord32le
getWord32 Motorola = getWord32be

parseExifBlock :: Get (Map ExifTag String)
parseExifBlock = do
	header <- getByteString 4
	nul <- liftM toInteger getWord16be
	unless (header == Char8.pack "Exif" && nul == 0)
		$ fail "invalid EXIF header"
	tiffHeaderStart <- liftM fromIntegral bytesRead
	byteAlign <- parseTiffHeader
	exifSubIfdOffset <- liftM (fromIntegral . toInteger) (parseIfd byteAlign)
	-- skip to the exif offset
	bytesReadNow <- liftM fromIntegral bytesRead
	skip $ (exifSubIfdOffset + tiffHeaderStart) - bytesReadNow
	parseExifSubIfd byteAlign tiffHeaderStart

parseTiffHeader :: Get ByteAlign
parseTiffHeader = do
	byteAlignV <- getByteString 2
	let byteAlign = case Char8.unpack byteAlignV of
		"II" -> Intel
		"MM" -> Motorola
		_ -> error "Unknown byte alignment"
	alignControl <- liftM toInteger (getWord16 byteAlign)
	unless (alignControl == 0x2a)
		$ fail "exif byte alignment mismatch"
	ifdOffset <- liftM (fromIntegral . toInteger) (getWord32 byteAlign)
	skip $ ifdOffset - 8
	return byteAlign

parseIfd :: ByteAlign -> Get Word32
parseIfd byteAlign = 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

parseExifSubIfd :: ByteAlign -> Int -> Get (Map ExifTag String)
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

data IfEntry = IfEntry
	{
		entryTag :: !Word16,
		entryFormat :: !Word16,
		entryNoComponents :: !Word32,
		entryContents :: !Word32
	} deriving Show

parseIfEntry :: ByteAlign -> Get IfEntry
parseIfEntry byteAlign = do
	tagNumber <- getWord16 byteAlign
	dataFormat <- getWord16 byteAlign
	numComponents <- getWord32 byteAlign
	value <- getWord32 byteAlign
	return IfEntry
		{
			entryTag = tagNumber,
			entryFormat = dataFormat,
			entryNoComponents = numComponents,
			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)

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

decodeEntry :: ByteAlign -> Int -> IfEntry -> Get (ExifTag, String)
decodeEntry byteAlign tiffHeaderStart entry = do
	let tagKey = getExifTag $ 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
		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
		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
		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
		-- 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)

word32toint32 :: Word32 -> Int32
word32toint32 word = fromIntegral word :: Int32

-- | Extract the date and time when the picture was taken
-- from the EXIF information.
getDateTimeOriginal :: Map ExifTag String -> Maybe LocalTime
getDateTimeOriginal exifData = do 
	dateStr <- 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))