hsexif 0.3.0.0 → 0.4.0.0
raw patch · 3 files changed
+506/−164 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Graphics.HsExif: ExifNumberList :: ![Int] -> ExifValue
+ Graphics.HsExif: ExifRationalList :: ![(Int, Int)] -> ExifValue
+ Graphics.HsExif: ExifUndefined :: !ByteString -> ExifValue
+ Graphics.HsExif: GpsSubIFD :: TagLocation
+ Graphics.HsExif: artist :: ExifTag
+ Graphics.HsExif: cfaPattern :: ExifTag
+ Graphics.HsExif: contrast :: ExifTag
+ Graphics.HsExif: customRendered :: ExifTag
+ Graphics.HsExif: dateTime :: ExifTag
+ Graphics.HsExif: deviceSettingDescription :: ExifTag
+ Graphics.HsExif: digitalZoomRatio :: ExifTag
+ Graphics.HsExif: exifIfdOffset :: ExifTag
+ Graphics.HsExif: exifInteroperabilityOffset :: ExifTag
+ Graphics.HsExif: exposureIndex :: ExifTag
+ Graphics.HsExif: exposureMode :: ExifTag
+ Graphics.HsExif: flashEnergy :: ExifTag
+ Graphics.HsExif: flashPixVersion :: ExifTag
+ Graphics.HsExif: focalLengthIn35mmFilm :: ExifTag
+ Graphics.HsExif: gainControl :: ExifTag
+ Graphics.HsExif: gpsAltitude :: ExifTag
+ Graphics.HsExif: gpsAltitudeRef :: ExifTag
+ Graphics.HsExif: gpsAreaInformation :: ExifTag
+ Graphics.HsExif: gpsDateStamp :: ExifTag
+ Graphics.HsExif: gpsDestBearing :: ExifTag
+ Graphics.HsExif: gpsDestBearingRef :: ExifTag
+ Graphics.HsExif: gpsDestDistance :: ExifTag
+ Graphics.HsExif: gpsDestDistanceRef :: ExifTag
+ Graphics.HsExif: gpsDestLatitude :: ExifTag
+ Graphics.HsExif: gpsDestLatitudeRef :: ExifTag
+ Graphics.HsExif: gpsDestLongitude :: ExifTag
+ Graphics.HsExif: gpsDestLongitudeRef :: ExifTag
+ Graphics.HsExif: gpsDifferential :: ExifTag
+ Graphics.HsExif: gpsDop :: ExifTag
+ Graphics.HsExif: gpsImgDirection :: ExifTag
+ Graphics.HsExif: gpsImgDirectionRef :: ExifTag
+ Graphics.HsExif: gpsLatitude :: ExifTag
+ Graphics.HsExif: gpsLatitudeRef :: ExifTag
+ Graphics.HsExif: gpsLongitude :: ExifTag
+ Graphics.HsExif: gpsLongitudeRef :: ExifTag
+ Graphics.HsExif: gpsMapDatum :: ExifTag
+ Graphics.HsExif: gpsMeasureMode :: ExifTag
+ Graphics.HsExif: gpsProcessingMethod :: ExifTag
+ Graphics.HsExif: gpsSatellites :: ExifTag
+ Graphics.HsExif: gpsSpeed :: ExifTag
+ Graphics.HsExif: gpsSpeedRef :: ExifTag
+ Graphics.HsExif: gpsStatus :: ExifTag
+ Graphics.HsExif: gpsTagOffset :: ExifTag
+ Graphics.HsExif: gpsTimeStamp :: ExifTag
+ Graphics.HsExif: gpsTrack :: ExifTag
+ Graphics.HsExif: gpsTrackRef :: ExifTag
+ Graphics.HsExif: gpsVersionID :: ExifTag
+ Graphics.HsExif: imageDescription :: ExifTag
+ Graphics.HsExif: imageUniqueId :: ExifTag
+ Graphics.HsExif: oecf :: ExifTag
+ Graphics.HsExif: primaryChromaticities :: ExifTag
+ Graphics.HsExif: printImageMatching :: ExifTag
+ Graphics.HsExif: referenceBlackWhite :: ExifTag
+ Graphics.HsExif: resolutionUnit :: ExifTag
+ Graphics.HsExif: saturation :: ExifTag
+ Graphics.HsExif: sceneCaptureType :: ExifTag
+ Graphics.HsExif: sharpness :: ExifTag
+ Graphics.HsExif: spatialFrequencyResponse :: ExifTag
+ Graphics.HsExif: spectralSensitivity :: ExifTag
+ Graphics.HsExif: subSecTime :: ExifTag
+ Graphics.HsExif: subSecTimeDigitized :: ExifTag
+ Graphics.HsExif: subSecTimeOriginal :: ExifTag
+ Graphics.HsExif: subjectArea :: ExifTag
+ Graphics.HsExif: subjectDistanceRange :: ExifTag
+ Graphics.HsExif: subjectLocation :: ExifTag
+ Graphics.HsExif: whiteBalance :: ExifTag
+ Graphics.HsExif: whitePoint :: ExifTag
+ Graphics.HsExif: xResolution :: ExifTag
+ Graphics.HsExif: yCbCrCoefficients :: ExifTag
+ Graphics.HsExif: yCbCrPositioning :: ExifTag
+ Graphics.HsExif: yResolution :: ExifTag
- Graphics.HsExif: ExifNumber :: Int -> ExifValue
+ Graphics.HsExif: ExifNumber :: !Int -> ExifValue
- Graphics.HsExif: ExifUnknown :: !Word16 -> !Int -> ExifValue
+ Graphics.HsExif: ExifUnknown :: !Word16 -> !Int -> !Int -> ExifValue
Files
- Graphics/HsExif.hs +444/−107
- hsexif.cabal +1/−1
- tests/Tests.hs +61/−56
Graphics/HsExif.hs view
@@ -17,43 +17,120 @@ -- * The ExifValue type ExifValue(..), - -- * All known exif tags+ -- * Most useful exif tags exposureTime, fnumber,- exposureProgram, isoSpeedRatings,- exifVersion, dateTimeOriginal,- dateTimeDigitized,- componentConfiguration,- compressedBitsPerPixel, shutterSpeedValue, apertureValue, brightnessValue, exposureBiasValue, maxApertureValue,- subjectDistance,- meteringMode,- lightSource, flash, focalLength,- makerNote, userComment,- colorSpace,+ orientation,+ make,+ model,+ software,+ copyright,+ digitalZoomRatio,+ focalLengthIn35mmFilm, + artist,++ -- * GPS related exif tags++ gpsVersionID,+ gpsLatitudeRef,+ gpsLatitude,+ gpsLongitudeRef,+ gpsLongitude,+ gpsAltitudeRef,+ gpsAltitude,+ gpsTimeStamp,+ gpsSatellites,+ gpsStatus,+ gpsMeasureMode,+ gpsDop,+ gpsSpeedRef,+ gpsSpeed,+ gpsTrackRef,+ gpsTrack,+ gpsImgDirectionRef,+ gpsImgDirection,+ gpsMapDatum,+ gpsDestLatitudeRef,+ gpsDestLatitude,+ gpsDestLongitudeRef,+ gpsDestLongitude,+ gpsDestBearingRef,+ gpsDestBearing,+ gpsDestDistanceRef,+ gpsDestDistance,+ gpsProcessingMethod,+ gpsAreaInformation,+ gpsDateStamp,+ gpsDifferential,++ -- * Less useful exif tags++ exifVersion,+ sensingMethod,+ fileSource,+ sceneType,+ makerNote,+ subjectDistance,+ meteringMode,+ lightSource, exifImageWidth, exifImageHeight, relatedSoundFile, focalPlaneXResolution, focalPlaneYResolution, focalPlaneResolutionUnit,- sensingMethod,- fileSource,- sceneType,- orientation,- make,- model,- software,- copyright,+ dateTimeDigitized,+ componentConfiguration,+ compressedBitsPerPixel,+ exposureProgram,+ spectralSensitivity,+ oecf,+ subjectArea,+ subSecTime,+ subSecTimeOriginal,+ subSecTimeDigitized,+ flashPixVersion,+ colorSpace,+ flashEnergy,+ spatialFrequencyResponse,+ subjectLocation,+ exposureIndex,+ cfaPattern,+ customRendered,+ exposureMode,+ whiteBalance,+ sceneCaptureType,+ gainControl,+ contrast,+ saturation,+ sharpness,+ deviceSettingDescription,+ subjectDistanceRange,+ imageUniqueId,+ exifInteroperabilityOffset,+ imageDescription,+ xResolution,+ yResolution,+ resolutionUnit,+ dateTime,+ whitePoint,+ primaryChromaticities,+ yCbCrPositioning,+ yCbCrCoefficients,+ referenceBlackWhite,+ exifIfdOffset,+ printImageMatching,+ gpsTagOffset, -- * If you need to declare your own exif tags ExifTag(..),@@ -61,29 +138,28 @@ ) where import Data.Binary.Get+import Data.Binary.Put import qualified Data.ByteString.Lazy as B+import qualified Data.ByteString as BS import Control.Monad (liftM, unless) import qualified Data.ByteString.Char8 as Char8 import Data.Word import Data.Char (isDigit, ord, chr)-import Data.Int (Int32)+import Data.Int (Int32, Int16, Int8) import Data.List-import Data.Maybe (fromMaybe)+import Data.Maybe (fromMaybe, fromJust) -- TODO try to get rid of fromJust 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!---+-- | An exif value.+-- -- 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.+data ExifValue = ExifNumber !Int+ -- ^ An exif number. Originally it could have been short, int, signed or not. | ExifText !String -- ^ ASCII text. | ExifRational !Int !Int@@ -91,18 +167,38 @@ -- Sometimes we're used to it as rational (exposition time: 1/160), -- sometimes as float (exposure compensation, we rather think -0.75) -- 'show' will display it as 1/160.- | ExifUnknown !Word16 !Int -- type then value- -- ^ Unknown exif value type. Maybe float? If the JPEG file is not- -- corrupted, please send it to me.+ | ExifNumberList ![Int]+ -- ^ List of numbers. Originally they could have been short, int, signed or not.+ | ExifRationalList ![(Int,Int)]+ -- ^ A list of rational numbers (numerator, denominator).+ -- Sometimes we're used to it as rational (exposition time: 1/160),+ -- sometimes as float (exposure compensation, we rather think -0.75)+ -- 'show' will display it as 1/160.+ | ExifUndefined !BS.ByteString+ -- ^ The undefined type in EXIF means that the contents are not+ -- specified and up to the manufacturer. In effect it's exactly+ -- a bytestring. Sometimes it's text with ASCII or UNICODE at the+ -- beginning, often it's binary in nature.+ | ExifUnknown !Word16 !Int !Int+ -- ^ Unknown exif value type. All EXIF 2.3 types are+ -- supported, it could be a newer file.+ -- The parameters are type, count then value 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+ show (ExifUnknown t c v) = show "Unknown exif type. Type: " ++ show t + ++ " count: " ++ show c ++ " value: " ++ show v+ show (ExifNumberList l) = show l+ show (ExifRationalList l) = show l+ show (ExifUndefined bs) = show bs -- see http://www.media.mit.edu/pia/Research/deepview/exif.html+-- and http://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf+-- and http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html+-- and http://www.exiv2.org/tags.html -- | Read EXIF data from the file you give. It's a key-value map. parseFileExif :: FilePath -> IO (Either String (Map ExifTag ExifValue))@@ -142,6 +238,10 @@ getWord32 Intel = getWord32le getWord32 Motorola = getWord32be +putWord32 :: ByteAlign -> (Word32 -> Put)+putWord32 Intel = putWord32le+putWord32 Motorola = putWord32be+ parseExifBlock :: Get (Map ExifTag ExifValue) parseExifBlock = do header <- getByteString 4@@ -150,14 +250,24 @@ $ fail "invalid EXIF header" tiffHeaderStart <- liftM fromIntegral bytesRead byteAlign <- parseTiffHeader- (exifSubIfdOffsetW, ifdEntries) <- parseIfd byteAlign tiffHeaderStart+ (exifSubIfdOffsetW, mGpsOffsetW, ifdEntries) <- parseIfd byteAlign tiffHeaderStart let exifSubIfdOffset = fromIntegral $ toInteger exifSubIfdOffsetW+ gpsData <- case mGpsOffsetW of+ Nothing -> return []+ Just gpsOffsetW -> lookAhead $ parseGps gpsOffsetW byteAlign tiffHeaderStart -- skip to the exif offset bytesReadNow <- liftM fromIntegral bytesRead skip $ (exifSubIfdOffset + tiffHeaderStart) - bytesReadNow- exifSubEntries <- parseExifSubIfd byteAlign tiffHeaderStart- return $ Map.fromList $ ifdEntries ++ exifSubEntries+ exifSubEntries <- parseSubIfd byteAlign tiffHeaderStart ExifSubIFD+ return $ Map.fromList $ ifdEntries ++ exifSubEntries ++ gpsData +parseGps :: Word32 -> ByteAlign -> Int -> Get [(ExifTag, ExifValue)]+parseGps gpsOffsetW byteAlign tiffHeaderStart = do+ let gpsOffset = fromIntegral $ toInteger gpsOffsetW+ bytesReadNow <- liftM fromIntegral bytesRead+ skip $ (gpsOffset + tiffHeaderStart) - bytesReadNow+ parseSubIfd byteAlign tiffHeaderStart GpsSubIFD+ parseTiffHeader :: Get ByteAlign parseTiffHeader = do byteAlignV <- getByteString 2@@ -172,27 +282,30 @@ skip $ ifdOffset - 8 return byteAlign -parseIfd :: ByteAlign -> Int -> Get (Word32, [(ExifTag, ExifValue)])+parseIfd :: ByteAlign -> Int -> Get (Word32, Maybe 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 exifOffsetEntry = fromMaybe (error "Can't find the exif ifd offset")+ (find (\ e -> entryTag e == tagKey exifIfdOffset) ifdEntries) let exifOffset = entryContents exifOffsetEntry++ let gpsOffsetEntry = find (\ e -> entryTag e == tagKey gpsTagOffset) ifdEntries+ let gpsOffset = fmap entryContents gpsOffsetEntry entries <- mapM (decodeEntry byteAlign tiffHeaderStart IFD0) ifdEntries- return (exifOffset, entries)+ return (exifOffset, gpsOffset, entries) -parseExifSubIfd :: ByteAlign -> Int -> Get [(ExifTag, ExifValue)]-parseExifSubIfd byteAlign tiffHeaderStart = do+parseSubIfd :: ByteAlign -> Int -> TagLocation -> Get [(ExifTag, ExifValue)]+parseSubIfd byteAlign tiffHeaderStart location = do dirEntriesCount <- liftM toInteger (getWord16 byteAlign) ifdEntries <- mapM (\_ -> parseIfEntry byteAlign) [1..dirEntriesCount]- mapM (decodeEntry byteAlign tiffHeaderStart ExifSubIFD) ifdEntries+ mapM (decodeEntry byteAlign tiffHeaderStart location) ifdEntries data IfEntry = IfEntry { entryTag :: !Word16, entryFormat :: !Word16,- entryNoComponents :: !Word32,+ entryNoComponents :: !Int, entryContents :: !Word32 } deriving Show @@ -206,7 +319,7 @@ { entryTag = tagNumber, entryFormat = dataFormat,- entryNoComponents = numComponents,+ entryNoComponents = fromIntegral $ toInteger numComponents, entryContents = value } @@ -216,7 +329,7 @@ -- exif tag you're interested in. -- Rather check the list of supported exif tags, like -- 'exposureTime' and so on.-data TagLocation = ExifSubIFD | IFD0+data TagLocation = ExifSubIFD | IFD0 | GpsSubIFD deriving (Show, Eq, Ord) -- | An exif tag. Normally you don't need to fiddle with this,@@ -248,95 +361,319 @@ exifIfd0Tag :: String -> Word16 -> ExifTag exifIfd0Tag d = ExifTag IFD0 (Just d) +exifGpsTag :: String -> Word16 -> ExifTag+exifGpsTag d = ExifTag GpsSubIFD (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 +spectralSensitivity = exifSubIfdTag "spectralSensitivity" 0x8824+isoSpeedRatings = exifSubIfdTag "isoSpeedRatings" 0x8827+oecf = exifSubIfdTag "OECF" 0x8828+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+subjectArea = exifSubIfdTag "subjectArea" 0x9214+makerNote = exifSubIfdTag "makerNote" 0x927c+userComment = exifSubIfdTag "userComment" 0x9286+subSecTime = exifSubIfdTag "subSecTime" 0x9290+subSecTimeOriginal = exifSubIfdTag "subSecTimeOriginal" 0x9291+subSecTimeDigitized = exifSubIfdTag "subSecTimeDigitized" 0x9292+flashPixVersion = exifSubIfdTag "flashPixVersion" 0xa000+colorSpace = exifSubIfdTag "colorSpace" 0xa001+exifImageWidth = exifSubIfdTag "exifImageWidth" 0xa002+exifImageHeight = exifSubIfdTag "exifImageHeight" 0xa003+relatedSoundFile = exifSubIfdTag "relatedSoundFile" 0xa004+flashEnergy = exifSubIfdTag "flashEnergy" 0xa20b+spatialFrequencyResponse= exifSubIfdTag "spatialFrequencyResponse" 0xa20c+focalPlaneXResolution = exifSubIfdTag "focalPlaneXResolution" 0xa20e+focalPlaneYResolution = exifSubIfdTag "focalPlaneYResolution" 0xa20f+focalPlaneResolutionUnit= exifSubIfdTag "focalPlaneResolutionUnit" 0xa210+subjectLocation = exifSubIfdTag "subjectLocation" 0xa214+exposureIndex = exifSubIfdTag "exposureIndex" 0xa215+sensingMethod = exifSubIfdTag "sensingMethod" 0xa217+fileSource = exifSubIfdTag "fileSource" 0xa300+sceneType = exifSubIfdTag "sceneType" 0xa301+cfaPattern = exifSubIfdTag "cfaPattern" 0xa302+customRendered = exifSubIfdTag "customRendered" 0xa401+exposureMode = exifSubIfdTag "exposureMode" 0xa402+whiteBalance = exifSubIfdTag "whiteBalance" 0xa403+digitalZoomRatio = exifSubIfdTag "digitalZoomRatio" 0xa404+focalLengthIn35mmFilm = exifSubIfdTag "focalLengthIn35mmFilm" 0xa405+sceneCaptureType = exifSubIfdTag "sceneCaptureType" 0xa406+gainControl = exifSubIfdTag "gainControl" 0xa407+contrast = exifSubIfdTag "contrast" 0xa408+saturation = exifSubIfdTag "saturation" 0xa409+sharpness = exifSubIfdTag "sharpness" 0xa40a+deviceSettingDescription= exifSubIfdTag "deviceSettingDescription" 0xa40b+subjectDistanceRange = exifSubIfdTag "subjectDistanceRange" 0xa40c+imageUniqueId = exifSubIfdTag "imageUniqueId" 0xa420+exifInteroperabilityOffset=exifSubIfdTag "exifInteroperabilityOffset" 0xa005 +imageDescription = exifIfd0Tag "imageDescription" 0x010e+make = exifIfd0Tag "make" 0x010f+model = exifIfd0Tag "model" 0x0110+orientation = exifIfd0Tag "orientation" 0x0112+xResolution = exifIfd0Tag "xResolution" 0x011a+yResolution = exifIfd0Tag "xResolution" 0x011b+resolutionUnit = exifIfd0Tag "resolutionUnit" 0x0128+software = exifIfd0Tag "software" 0x0131+dateTime = exifIfd0Tag "dateTime" 0x0132+artist = exifIfd0Tag "artist" 0x013b+whitePoint = exifIfd0Tag "whitePoint" 0x013e+primaryChromaticities = exifIfd0Tag "primaryChromaticities" 0x013f+yCbCrCoefficients = exifIfd0Tag "yCbCrCoefficients" 0x0211+yCbCrPositioning = exifIfd0Tag "yCbCrPositioning" 0x0213+referenceBlackWhite = exifIfd0Tag "referenceBlackWhite" 0x0214+copyright = exifIfd0Tag "copyright" 0x8298+exifIfdOffset = exifIfd0Tag "exifIfdOffset" 0x8769+gpsTagOffset = exifIfd0Tag "gpsTagOffset" 0x8825+printImageMatching = exifIfd0Tag "printImageMatching" 0xc4a5++gpsVersionID = exifGpsTag "gpsVersionID" 0x0000+gpsLatitudeRef = exifGpsTag "gpsLatitudeRef" 0x0001+gpsLatitude = exifGpsTag "gpsLatitude" 0x0002+gpsLongitudeRef = exifGpsTag "gpsLongitudeRef" 0x0003+gpsLongitude = exifGpsTag "gpsLongitude" 0x0004+gpsAltitudeRef = exifGpsTag "gpsAltitudeRef" 0x0005+gpsAltitude = exifGpsTag "gpsAltitude" 0x0006+gpsTimeStamp = exifGpsTag "gpsTimeStamp" 0x0007+gpsSatellites = exifGpsTag "gpsSatellites" 0x0008+gpsStatus = exifGpsTag "gpsStatus" 0x0009+gpsMeasureMode = exifGpsTag "gpsMeasureMode" 0x000a+gpsDop = exifGpsTag "gpsDop" 0x000b+gpsSpeedRef = exifGpsTag "gpsSpeedRef" 0x000c+gpsSpeed = exifGpsTag "gpsSpeed" 0x000d+gpsTrackRef = exifGpsTag "gpsTrackRef" 0x000e+gpsTrack = exifGpsTag "gpsTrack" 0x000f+gpsImgDirectionRef= exifGpsTag "gpsImgDirectionRef" 0x0010+gpsImgDirection = exifGpsTag "gpsImgDirection" 0x0011+gpsMapDatum = exifGpsTag "gpsMapDatum" 0x0012+gpsDestLatitudeRef= exifGpsTag "gpsDestLatitudeRef" 0x0013+gpsDestLatitude = exifGpsTag "gpsDestLatitude" 0x0014+gpsDestLongitudeRef= exifGpsTag "gpsDestLongitudeRef" 0x0015+gpsDestLongitude= exifGpsTag "gpsDestLongitude" 0x0016+gpsDestBearingRef= exifGpsTag "gpsDestBearingRef" 0x0017+gpsDestBearing = exifGpsTag "gpsDestBearing" 0x0018+gpsDestDistanceRef= exifGpsTag "gpsDestDistanceRef" 0x0019+gpsDestDistance = exifGpsTag "gpsDestDistance" 0x001a+gpsProcessingMethod= exifGpsTag "gpsProcessingMethod" 0x001b+gpsAreaInformation= exifGpsTag "gpsAreaInformation" 0x001c+gpsDateStamp = exifGpsTag "gpsDateStamp" 0x001d+gpsDifferential = exifGpsTag "gpsDifferential" 0x001e+ 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,+ lightSource, flash, focalLength, makerNote, userComment, exifImageWidth, exifImageHeight, relatedSoundFile, focalPlaneXResolution, focalPlaneYResolution, focalPlaneResolutionUnit, sensingMethod, fileSource,- sceneType, orientation, make, model, software, copyright]+ sceneType, orientation, make, model, software, copyright,+ spectralSensitivity, oecf, subjectArea, subSecTime, subSecTimeOriginal,+ subSecTimeDigitized, flashPixVersion, colorSpace, flashEnergy,+ spatialFrequencyResponse, subjectLocation, exposureIndex, cfaPattern,+ customRendered, exposureMode, whiteBalance, digitalZoomRatio,+ focalLengthIn35mmFilm, sceneCaptureType, gainControl, contrast,+ saturation, sharpness, deviceSettingDescription, subjectDistanceRange,+ imageUniqueId, exifInteroperabilityOffset, imageDescription,+ xResolution, yResolution, resolutionUnit, dateTime, whitePoint,+ primaryChromaticities, yCbCrPositioning, yCbCrCoefficients, referenceBlackWhite,+ exifIfdOffset, printImageMatching, gpsTagOffset, artist,+ gpsVersionID, gpsLatitudeRef, gpsLatitude, gpsLongitudeRef, gpsLongitude,+ gpsAltitudeRef, gpsAltitude, gpsTimeStamp, gpsSatellites, gpsStatus,+ gpsMeasureMode, gpsDop, gpsSpeedRef, gpsSpeed, gpsTrackRef, gpsTrack,+ gpsImgDirectionRef, gpsImgDirection, gpsMapDatum, gpsDestLatitudeRef,+ gpsDestLatitude, gpsDestLongitudeRef, gpsDestLongitude, gpsDestBearingRef,+ gpsDestBearing, gpsDestDistanceRef, gpsDestDistance, gpsProcessingMethod,+ gpsAreaInformation, gpsDateStamp, gpsDifferential] 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 +data ValueHandler = ValueHandler+ {+ dataTypeId :: Word16,+ dataLength :: Int,+ readSingle :: ByteAlign -> Get ExifValue,+ readMany :: ByteAlign -> Int -> Get ExifValue+ }++readNumberList decoder = \byteAlign components -> liftM (ExifNumberList . fmap fromIntegral)+ $ readManyInternal byteAlign components+ where readManyInternal byteAlign components = count components (decoder byteAlign)++unsignedByteValueHandler = ValueHandler+ {+ dataTypeId = 1,+ dataLength = 1,+ readSingle = \_ -> liftM (ExifNumber . fromIntegral) getWord8,+ readMany = readNumberList (\_ -> getWord8)+ }++asciiStringValueHandler = ValueHandler+ {+ dataTypeId = 2,+ dataLength = 1,+ readSingle = \ba -> readMany asciiStringValueHandler ba 1,+ readMany = \_ components -> liftM (ExifText . Char8.unpack) (getByteString (components-1))+ }++unsignedShortValueHandler = ValueHandler+ {+ dataTypeId = 3,+ dataLength = 2,+ readSingle = \byteAlign -> liftM (ExifNumber . fromIntegral) $ getWord16 byteAlign,+ readMany = readNumberList getWord16+ }++unsignedLongValueHandler = ValueHandler+ {+ dataTypeId = 4,+ dataLength = 4,+ readSingle = \byteAlign -> liftM (ExifNumber . fromIntegral) $ getWord32 byteAlign,+ readMany = readNumberList getWord32+ }++readRationalContents :: (Int -> Int -> a) -> ByteAlign -> Get a+readRationalContents c byteAlign = do+ numerator <- liftM fromIntegral $ getWord32 byteAlign+ denominator <- liftM fromIntegral $ getWord32 byteAlign+ return $ c numerator denominator++unsignedRationalValueHandler = ValueHandler+ {+ dataTypeId = 5,+ dataLength = 8,+ readSingle = readRationalContents ExifRational,+ readMany = let readManyInternal byteAlign components = count components (readRationalContents (,) byteAlign) in+ \byteAlign components -> liftM ExifRationalList $ readManyInternal byteAlign components+ }++signedByteValueHandler = ValueHandler+ {+ dataTypeId = 6,+ dataLength = 1,+ readSingle = \_ -> liftM (ExifNumber . signedInt8ToInt) getWord8,+ readMany = readNumberList (\_ -> liftM signedInt8ToInt getWord8)+ }++undefinedValueHandler = ValueHandler+ {+ dataTypeId = 7,+ dataLength = 1,+ readSingle = \ba -> readMany undefinedValueHandler ba 1,+ readMany = \_ components -> liftM ExifUndefined (getByteString (components-1))+ }++signedShortValueHandler = ValueHandler+ {+ dataTypeId = 8,+ dataLength = 2,+ readSingle = \byteAlign -> liftM (ExifNumber . signedInt16ToInt) $ getWord16 byteAlign,+ readMany = readNumberList (\b -> liftM signedInt16ToInt $ getWord16 b)+ }++signedLongValueHandler = ValueHandler+ {+ dataTypeId = 9,+ dataLength = 4,+ readSingle = \byteAlign -> liftM (ExifNumber . signedInt32ToInt) $ getWord32 byteAlign,+ readMany = readNumberList (\b -> liftM signedInt32ToInt $ getWord32 b)+ }++readSignedRationalContents :: (Int -> Int -> a) -> ByteAlign -> Get a+readSignedRationalContents c byteAlign = do+ numerator <- liftM signedInt32ToInt $ getWord32 byteAlign+ denominator <- liftM signedInt32ToInt $ getWord32 byteAlign+ return $ c numerator denominator++signedRationalValueHandler = ValueHandler+ {+ dataTypeId = 10,+ dataLength = 8,+ readSingle = readSignedRationalContents ExifRational,+ readMany = let readManyInternal byteAlign components = count components (readSignedRationalContents (,) byteAlign) in+ \byteAlign components -> liftM ExifRationalList $ readManyInternal byteAlign components+ }++-- ascii string is special for now.+valueHandlers :: [ValueHandler]+valueHandlers =+ [+ unsignedByteValueHandler,+ asciiStringValueHandler,+ unsignedShortValueHandler,+ unsignedLongValueHandler,+ unsignedRationalValueHandler,+ signedByteValueHandler,+ signedShortValueHandler,+ signedLongValueHandler,+ signedRationalValueHandler,+ undefinedValueHandler+ ]+ 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 $ ExifNumber contentsInt -- unsigned byte- 2 -> do -- ascii string- curPos <- liftM fromIntegral bytesRead- skip $ contentsInt + tiffHeaderStart - curPos- 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 <- 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 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 $ ExifUnknown (entryFormat entry) contentsInt+ tagValue <- case getHandler $ entryFormat entry of+ Just handler -> decodeEntryWithHandler byteAlign tiffHeaderStart handler entry+ Nothing -> return $ ExifUnknown (entryFormat entry) (entryNoComponents entry) contentsInt return (exifTag, tagValue) +getHandler :: Word16 -> Maybe ValueHandler+getHandler typeId = find ((==typeId) . dataTypeId) valueHandlers++decodeEntryWithHandler :: ByteAlign -> Int -> ValueHandler -> IfEntry -> Get ExifValue+decodeEntryWithHandler byteAlign tiffHeaderStart handler entry = do+ if dataLength handler * (entryNoComponents entry) <= 4+ then return $ parseInline byteAlign handler entry inlineBs+ else parseOffset byteAlign tiffHeaderStart handler entry+ where+ inlineBs = runPut $ putWord32 byteAlign $ entryContents entry++parseInline :: ByteAlign -> ValueHandler -> IfEntry -> B.ByteString -> ExifValue+parseInline byteAlign handler entry bytestring =+ fromJust $ runMaybeGet getter bytestring+ where+ getter = case entryNoComponents entry of+ 1 -> readSingle handler byteAlign+ _ -> readMany handler byteAlign $ entryNoComponents entry++parseOffset :: ByteAlign -> Int -> ValueHandler -> IfEntry -> Get ExifValue+parseOffset byteAlign tiffHeaderStart handler entry = do+ let contentsInt = fromIntegral $ toInteger $ entryContents entry+ curPos <- liftM fromIntegral bytesRead+ skip $ contentsInt + tiffHeaderStart - curPos+ bytestring <- getLazyByteString (fromIntegral $ entryNoComponents entry * dataLength handler)+ return $ parseInline byteAlign handler entry bytestring+ signedInt32ToInt :: Word32 -> Int signedInt32ToInt w = fromIntegral (fromIntegral w :: Int32)++signedInt16ToInt :: Word16 -> Int+signedInt16ToInt w = fromIntegral (fromIntegral w :: Int16)++signedInt8ToInt :: Word8 -> Int+signedInt8ToInt w = fromIntegral (fromIntegral w :: Int8) -- i had this as runGetM and reusing in parseExif, -- sadly fail is not implemented for Either.
hsexif.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: hsexif-version: 0.3.0.0+version: 0.4.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
@@ -1,13 +1,15 @@+{-# LANGUAGE OverloadedStrings #-} import Test.Hspec import Test.HUnit import Graphics.HsExif import qualified Data.ByteString.Lazy as B+import qualified Data.ByteString as BS import qualified Data.Map as Map import Data.Map (Map) import Data.Time.LocalTime import Data.Time.Calendar-import Data.Word+import Data.Char (chr) main :: IO () main = do@@ -33,61 +35,64 @@ assertEqual' (Left "No EXIF in JPEG") (parseExif imageContents) testBasic :: B.ByteString -> Spec-testBasic imageContents = it "parses a simple JPEG" $- assertEqual' (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)--unknownExifSubIfdTag :: Word16 -> ExifTag-unknownExifSubIfdTag = ExifTag ExifSubIFD Nothing--unknownExifIfd0Tag :: Word16 -> ExifTag-unknownExifIfd0Tag = ExifTag IFD0 Nothing+testBasic imageContents = it "parses a simple JPEG" $ do+ assertEqual' + (Map.fromList+ [+ (exposureTime, ExifRational 1 160),+ (fnumber, ExifRational 0 10),+ (exposureProgram, ExifNumber 2),+ (isoSpeedRatings, ExifNumber 1600),+ (exifVersion, ExifUndefined "023"),+ (dateTimeOriginal, ExifText "2013:10:02 20:33:33"),+ (dateTimeDigitized, ExifText "2013:10:02 20:33:33"),+ (componentConfiguration, ExifUndefined "\SOH\STX\ETX"),+ (compressedBitsPerPixel, ExifRational 2 1),+ (brightnessValue, ExifRational (-4226) 2560),+ (exposureBiasValue, ExifRational 0 10),+ (maxApertureValue, ExifRational 0 10),+ (meteringMode, ExifNumber 5),+ (lightSource, ExifNumber 0),+ (fileSource, ExifUndefined ""),+ (flash, ExifNumber 24),+ (focalLength, ExifRational 0 10),+ (userComment, ExifUndefined "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL"),+ (flashPixVersion, ExifUndefined "010"),+ (colorSpace, ExifNumber 1),+ (sceneType, ExifUndefined ""),+ (exifImageWidth, ExifNumber 1),+ (exifImageHeight, ExifNumber 1),+ (exifInteroperabilityOffset, ExifNumber 36640),+ (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),+ (dateTime, ExifText "2014:04:10 20:14:20"),+ (yCbCrPositioning, ExifNumber 2),+ (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")+ ]) cleanedParsed+ -- the sony maker note is 35k!! Just test its size and that it starts with "SONY DSC".+ assertEqual' 35698 (BS.length makerNoteV)+ assertEqual' "SONY DSC" (take 8 $ fmap (chr . fromIntegral) $ BS.unpack makerNoteV)+ where+ -- the makerNote is HUGE. so test it separately.+ parsed = (\(Right x) -> x) $ parseExif imageContents+ cleanedParsed = Map.fromList $ filter (\(a,_) -> not (a==makerNote)) $ Map.toList parsed+ makerNoteV = (\(Just (ExifUndefined x)) -> x) $ Map.lookup makerNote parsed testDate :: Map ExifTag ExifValue -> Spec testDate exifData = it "extracts the date correctly" $