hsexif-0.1.0.0: 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, when)
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 (bs,offset,errorMsg) -> Left errorMsg
Right (bs,offset,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 dataSize
-- 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 :: Int -> Get (Map ExifTag String)
parseExifBlock blockLength = do
header <- getByteString 4
null <- liftM toInteger getWord16be
unless (header == Char8.pack "Exif" && null == 0)
$ fail "invalid EXIF header"
tiffHeaderStart <- liftM fromIntegral bytesRead
byteAlign <- parseTiffHeader
exifSubIfdOffset <- liftM (fromIntegral . toInteger) (parseIfd byteAlign tiffHeaderStart)
-- 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
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 -> Int -> Get Word32
parseIfd byteAlign tiffHeaderStart = do
dirEntriesCount <- liftM toInteger (getWord16 byteAlign)
ifdEntries <- mapM (\_ -> parseIfEntry byteAlign tiffHeaderStart) [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 tiffHeaderStart) [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 -> Int -> Get IfEntry
parseIfEntry byteAlign tiffHeaderStart = 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 entryTag = case entryTag 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 entryTag
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))