hsexif 0.6.1.9 → 0.6.1.10
raw patch · 6 files changed
+128/−31 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- Graphics/ExifTags.hs +18/−5
- Graphics/Helpers.hs +0/−1
- Graphics/HsExif.hs +29/−11
- Graphics/PrettyPrinters.hs +6/−6
- hsexif.cabal +1/−1
- tests/Tests.hs +74/−7
Graphics/ExifTags.hs view
@@ -35,7 +35,7 @@ showT :: Show a => a -> Text showT = T.pack . show -exposureTime = exifSubIfdTag "exposureTime" 0x829a $ ppExposureTime+exposureTime = exifSubIfdTag "exposureTime" 0x829a ppExposureTime fnumber = exifSubIfdTag "fnumber" 0x829d ppAperture exposureProgram = exifSubIfdTag "exposureProgram" 0x8822 ppExposureProgram spectralSensitivity = exifSubIfdTag "spectralSensitivity" 0x8824 showT@@ -46,7 +46,7 @@ dateTimeDigitized = exifSubIfdTag "dateTimeDigitized" 0x9004 showT componentConfiguration = exifSubIfdTag "componentConfiguration" 0x9101 ppComponentConfiguration compressedBitsPerPixel = exifSubIfdTag "compressedBitsPerPixel" 0x9102 (T.pack . formatAsFloatingPoint 2)-shutterSpeedValue = exifSubIfdTag "shutterSpeedValue" 0x9201 $ ppExposureTime+shutterSpeedValue = exifSubIfdTag "shutterSpeedValue" 0x9201 ppExposureTime apertureValue = exifSubIfdTag "apertureValue" 0x9202 ppApexAperture brightnessValue = exifSubIfdTag "brightnessValue" 0x9203 $ asFpWithFormat "%s EV" exposureBiasValue = exifSubIfdTag "exposureBiasValue" 0x9204 $ asFpWithFormat "%s EV"@@ -92,6 +92,15 @@ subjectDistanceRange = exifSubIfdTag "subjectDistanceRange" 0xa40c ppSubjectDistanceRange imageUniqueId = exifSubIfdTag "imageUniqueId" 0xa420 showT exifInteroperabilityOffset=exifSubIfdTag "exifInteroperabilityOffset" 0xa005 showT+offsetTime = exifSubIfdTag "offsetTime" 0x9010 showT+offsetTimeDigitized = exifSubIfdTag "Offset Time Digitized" 0x9012 showT+offsetTimeOriginal = exifSubIfdTag "Offset Time Original" 0x9011 showT+lensSpecification = exifSubIfdTag "lensSpecification" 0xa432 showT -- also called lensInfo+lensMake = exifSubIfdTag "lensMake" 0xa433 showT+lensModel = exifSubIfdTag "lensModel" 0xa434 showT+compositeImage = exifSubIfdTag "compositeImage" 0xa460 showT+compositeImageCount = exifSubIfdTag "compositeImageCount" 0xa461 showT+compositeImageExposureTimes = exifSubIfdTag "compositeImageExposureTimes" 0xa462 showT imageDescription = exifIfd0Tag "imageDescription" 0x010e showT make = exifIfd0Tag "make" 0x010f showT@@ -110,6 +119,7 @@ referenceBlackWhite = exifIfd0Tag "referenceBlackWhite" 0x0214 showT copyright = exifIfd0Tag "copyright" 0x8298 showT printImageMatching = exifIfd0Tag "printImageMatching" 0xc4a5 ppUndef+hostComputer = exifIfd0Tag "hostComputer" 0x13c showT gpsVersionID = exifGpsTag "gpsVersionID" 0x0000 showT gpsLatitudeRef = exifGpsTag "gpsLatitudeRef" 0x0001 ppGpsLatitudeRef@@ -168,7 +178,10 @@ gpsImgDirectionRef, gpsImgDirection, gpsMapDatum, gpsDestLatitudeRef, gpsDestLatitude, gpsDestLongitudeRef, gpsDestLongitude, gpsDestBearingRef, gpsDestBearing, gpsDestDistanceRef, gpsDestDistance, gpsProcessingMethod,- gpsAreaInformation, gpsDateStamp, gpsDifferential]+ gpsAreaInformation, gpsDateStamp, gpsDifferential,+ offsetTime, offsetTimeDigitized, offsetTimeOriginal, lensSpecification,+ lensMake, lensModel, compositeImage, compositeImageCount, compositeImageExposureTimes,+ hostComputer] -- | Extract the GPS latitude and longitude where the picture was taken -- (if it is present in the EXIF)@@ -238,9 +251,9 @@ ppGpsLatitudeRef :: ExifValue -> Text ppGpsLatitudeRef (ExifText "N") = "North" ppGpsLatitudeRef (ExifText "S") = "South"-ppGpsLatitudeRef v@_ = T.pack $ "Invalid latitude: " ++ show v+ppGpsLatitudeRef v = T.pack $ "Invalid latitude: " ++ show v ppGpsLongitudeRef :: ExifValue -> Text ppGpsLongitudeRef (ExifText "E") = "East" ppGpsLongitudeRef (ExifText "W") = "West"-ppGpsLongitudeRef v@_ = T.pack $ "Invalid longitude: " ++ show v+ppGpsLongitudeRef v = T.pack $ "Invalid longitude: " ++ show v
Graphics/Helpers.hs view
@@ -3,7 +3,6 @@ import Data.Binary.Get import qualified Data.ByteString.Lazy as B import Control.Monad (replicateM)-import Control.Applicative ( (<$>) ) import Data.Char (chr, isDigit, ord) -- | Suppress the 'Left' value of an 'Either'
Graphics/HsExif.hs view
@@ -147,7 +147,6 @@ import qualified Data.ByteString.Lazy as B import qualified Data.ByteString as BS import Control.Monad-import Control.Applicative ( (<$>) ) import qualified Data.ByteString.Char8 as Char8 import Data.ByteString.Internal (w2c) import Data.Word@@ -177,7 +176,7 @@ -- The reading is strict to avoid file handle exhaustion on a recursive -- reading of a directory tree. parseFileExif :: FilePath -> IO (Either String (Map ExifTag ExifValue))-parseFileExif filename = withFile filename ReadMode ((evaluate =<<) . fmap parseExif . B.hGetContents)+parseFileExif filename = withFile filename ReadMode ((evaluate . parseExif) <=< B.hGetContents) -- | Read EXIF data from a lazy bytestring. parseExif :: B.ByteString -> Either String (Map ExifTag ExifValue)@@ -186,6 +185,15 @@ getExif :: Get (Map ExifTag ExifValue) getExif = do firstBytes <- lookAhead $ (,) <$> getWord16be <*> getWord16be++ ftypheic <- lookAhead $ do+ skip 4+ ftyp <- getByteString 4+ skip 8+ mif <- getByteString 4+ -- https://github.com/kamadak/exif-rs/blob/b279fa00a80a1136c10e2c6b3980a040adc92f05/src/isobmff.rs#L33-L37+ return $ ftyp == "ftyp" && mif `elem` ["mif1", "msf1"]+ case firstBytes of (0xffd8,_ ) -> getWord16be >> findAndParseExifBlockJPEG (0x4d4d,0x002A) -> findAndParseExifBlockTiff -- TIFF big-endian: DNG, Nikon@@ -196,7 +204,9 @@ (0x4949,0x5500) -> findAndParseExifBlockTiff -- Panasonic RW2 -- Fuji RAF files use a custom format with an embedded JPEG preview containing the EXIF data (0x4655,0x4A49) -> findAndParseExifBlockFuji -- Fuji RAF- _ -> fail "Not a JPEG, TIFF, RAF, or TIFF-based raw file"+ _ -> if ftypheic+ then findAndParseExifBlockHEIC+ else fail "Not a JPEG, TIFF, RAF, or TIFF-based raw file" findAndParseExifBlockJPEG :: Get (Map ExifTag ExifValue) findAndParseExifBlockJPEG = do@@ -231,6 +241,14 @@ + 2 -- findAndParseExifBlockJPEG expects 2 bytes skipped findAndParseExifBlockJPEG +findAndParseExifBlockHEIC :: Get (Map ExifTag ExifValue)+findAndParseExifBlockHEIC = do+ (e, nul, aligned) <- lookAhead $ (,,) <$> getByteString 4 <*> (toInteger <$> getWord16be) <*> getByteString 2++ if e == "Exif" && nul == 0 && aligned `elem` ["MM", "II"]+ then skip 6 >> parseTiff+ else skip 1 >> findAndParseExifBlockHEIC+ data ByteAlign = Intel | Motorola deriving (Eq) getWord16 :: ByteAlign -> Get Word16@@ -280,7 +298,7 @@ skip offset dirEntriesCount <- fromIntegral <$> getWord16 byteAlign replicateM dirEntriesCount (parseIfEntry byteAlign ifdId)- + concat <$> mapM (entryTags byteAlign) entries -- | Convert IFD entries to tags, reading sub-IFDs@@ -347,7 +365,7 @@ { dataTypeId = 3, dataLength = 2,- readSingle = liftM (ExifNumber . fromIntegral) . getWord16,+ readSingle = fmap (ExifNumber . fromIntegral) . getWord16, readMany = readNumberList getWord16 } @@ -355,7 +373,7 @@ { dataTypeId = 4, dataLength = 4,- readSingle = liftM (ExifNumber . fromIntegral) . getWord32,+ readSingle = fmap (ExifNumber . fromIntegral) . getWord32, readMany = readNumberList getWord32 } @@ -378,7 +396,7 @@ dataTypeId = 6, dataLength = 1, readSingle = \_ -> ExifNumber . signedInt8ToInt <$> getWord8,- readMany = readNumberList (liftM signedInt8ToInt . const getWord8)+ readMany = readNumberList (fmap signedInt8ToInt . const getWord8) } undefinedValueHandler = ValueHandler@@ -393,16 +411,16 @@ { dataTypeId = 8, dataLength = 2,- readSingle = liftM (ExifNumber . signedInt16ToInt) . getWord16,- readMany = readNumberList (liftM signedInt16ToInt . getWord16)+ readSingle = fmap (ExifNumber . signedInt16ToInt) . getWord16,+ readMany = readNumberList (fmap signedInt16ToInt . getWord16) } signedLongValueHandler = ValueHandler { dataTypeId = 9, dataLength = 4,- readSingle = liftM (ExifNumber . signedInt32ToInt) . getWord32,- readMany = readNumberList (liftM signedInt32ToInt . getWord32)+ readSingle = fmap (ExifNumber . signedInt32ToInt) . getWord32,+ readMany = readNumberList (fmap signedInt32ToInt . getWord32) } readSignedRationalContents :: (Int -> Int -> a) -> ByteAlign -> Get a
Graphics/PrettyPrinters.hs view
@@ -52,7 +52,7 @@ ppExposureTime :: ExifValue -> Text ppExposureTime v@(ExifRational num den) = let seconds = fromIntegral num / (fromIntegral den :: Double)- value | seconds <= 0.25 && seconds > 0 = "1/" ++ show ((round (1 / seconds)) :: Int)+ value | seconds <= 0.25 && seconds > 0 = "1/" ++ show (round (1 / seconds) :: Int) | otherwise = formatAsNumber 1 v in T.append (T.pack value) " sec." ppExposureTime v = T.pack (show v)@@ -206,7 +206,7 @@ where numbers = fromIntegral <$> BS.unpack bs formatComponent = fromMaybe "?" . flip Map.lookup componentMap-ppComponentConfiguration v@_ = unknown v+ppComponentConfiguration v = unknown v ppFlashPixVersion :: ExifValue -> Text ppFlashPixVersion = formatVersion "FlashPix version %.1f"@@ -219,26 +219,26 @@ where num :: Float = read asStr / 100.0 asStr = T.unpack $ decodeUtf8 s-formatVersion _ v@_ = unknown v+formatVersion _ v = unknown v #if ICONV getIconvEncodingName :: Text -> EncodingName getIconvEncodingName "JIS" = "SJIS"-getIconvEncodingName x@_ = T.unpack x -- ASCII and UNICODE work out of the box.+getIconvEncodingName x = T.unpack x -- ASCII and UNICODE work out of the box. ppUserComment :: ExifValue -> Text ppUserComment (ExifUndefined v) = decodeUtf8 $ BL.toStrict $ convertFuzzy Transliterate encoding "UTF8" rawText where encoding = getIconvEncodingName $ decodeUtf8 $ BS.take 8 v rawText = BL.fromStrict $ BS.drop 8 v-ppUserComment v@_ = unknown v+ppUserComment v = unknown v #else ppUserComment :: ExifValue -> Text ppUserComment (ExifUndefined v) = decodeUtf8 $ BS.drop 8 v-ppUserComment v@_ = unknown v+ppUserComment v = unknown v #endif
hsexif.cabal view
@@ -1,5 +1,5 @@ name: hsexif-version: 0.6.1.9+version: 0.6.1.10 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
@@ -11,14 +11,13 @@ import Data.Time.LocalTime import Data.Time.Calendar import Data.Char (chr)-import Control.Monad (join)-import Control.Applicative ( (<$>) ) import Data.List import Graphics.Types (formatAsRational) import Graphics.PrettyPrinters (ppExposureTime) import Graphics.HsExif import Graphics.Helpers+import Graphics.ExifTags main :: IO () main = do@@ -30,6 +29,7 @@ gps3 <- B.readFile "tests/gps3.jpg" partial <- B.readFile "tests/partial_exif.jpg" tiff <- B.readFile "tests/RAW_NIKON_D1.NEF"+ appleHEIC <- B.readFile "tests/Apple_iPhone14.HEIC" let parseExifM = hush . parseExif let exifData = parseExifM imageContents let gpsExifData = parseExifM gps@@ -54,6 +54,7 @@ describe "flash fired" $ testFlashFired exifData describe "partial exif data" $ testPartialExif partial describe "tiff file" $ testNef tiffExifData+ describe "Apple HEIC file" $ testHEIC appleHEIC describe "unusual data layouts" $ do it "parses EXIF below IDF0" $ do@@ -61,14 +62,13 @@ case result of Left err -> assertBool ("Cannot parse: " ++ err) False Right tags -> do- (Map.lookup dateTimeOriginal tags) `assertEqual'` (Just $ ExifText "2013:10:02 20:33:33")+ Map.lookup dateTimeOriginal tags `assertEqual'` Just (ExifText "2013:10:02 20:33:33") it "xmp block before the exif" $ do result <- parseFileExif "tests/xmp-before-exif-truncated.jpg" -- the file is truncated. original is at: https://github.com/emmanueltouzery/hsexif/issues/17 case result of Left err -> assertBool ("Cannot parse: " ++ err) False Right tags -> do- (Map.lookup dateTimeOriginal tags) `assertEqual'` (Just $ ExifText "2020:03:04 17:14:20")-+ Map.lookup dateTimeOriginal tags `assertEqual'` Just (ExifText "2020:03:04 17:14:20") testNotAJpeg :: B.ByteString -> Spec testNotAJpeg imageContents = it "returns empty list if not a JPEG" $@@ -131,7 +131,7 @@ ]) (sort cleanedParsed) -- the sony maker note is 35k!! Just test its size and that it starts with "SONY DSC". assertEqual' (Just 35692) (BS.length <$> makerNoteV)- assertEqual' (Just "SONY DSC") (take 8 <$> fmap (chr . fromIntegral) <$> BS.unpack <$> makerNoteV)+ assertEqual' (Just "SONY DSC") (take 8 . fmap (chr . fromIntegral) . BS.unpack <$> makerNoteV) where -- the makerNote is HUGE. so test it separately. parsed = hush $ parseExif imageContents@@ -166,7 +166,7 @@ testReadGpsLatLongNoData :: Maybe (Map ExifTag ExifValue) -> Spec testReadGpsLatLongNoData exifData = it "reads gps latitude longitude" $- assertEqual' Nothing $ join $ getGpsLatitudeLongitude <$> exifData+ assertEqual' Nothing $ getGpsLatitudeLongitude =<< exifData testFormatAsFloatingPoint :: Spec testFormatAsFloatingPoint = it "properly formats as floating point" $ do@@ -308,3 +308,70 @@ assertEqual' :: (Show a, Eq a) => a -> a -> Assertion assertEqual' = assertEqual "doesn't match"++testHEIC :: B.ByteString -> Spec+testHEIC imageContents = it "parses an Apple HEIC" $ do+ case cleanedParsedM of+ Nothing -> assertBool "Parsing fails" False+ Just cleanedParsed -> do+ assertEqualListDebug (sort expected) (sort cleanedParsed)++ assertEqual'+ (Just "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\SOH")+ (take 8 . fmap (chr . fromIntegral) . BS.unpack <$> compositeImageExposureTimesV)++ assertEqual' (Just 1514) (BS.length <$> makerNoteV)++ where+ parsed = hush $ parseExif imageContents+ tricky = [compositeImageExposureTimes, makerNote]+ cleanedParsedM = filter ((`notElem` tricky) . fst) <$> (Map.toList <$> parsed)++ makerNoteV = parsed >>= Map.lookup makerNote >>= getUndefMaybe+ compositeImageExposureTimesV = parsed >>= Map.lookup compositeImageExposureTimes >>= getUndefMaybe++ expected =+ [ (hostComputer, ExifText "iPhone 14 Pro Max")+ , (exposureTime, ExifRational 1 4)+ , (fnumber, ExifRational 11 5)+ , (exposureProgram, ExifNumber 2)+ , (isoSpeedRatings, ExifNumber 2500)+ , (exifVersion, ExifUndefined "0232")+ , (dateTimeOriginal, ExifText "2023:05:07 19:21:26")+ , (dateTimeDigitized, ExifText "2023:05:07 19:21:26")+ , (offsetTime, ExifText "+08:00")+ , (offsetTimeDigitized, ExifText "+08:00")+ , (offsetTimeOriginal, ExifText "+08:00")+ , (shutterSpeedValue, ExifRational 17962 9509)+ , (apertureValue, ExifRational 193685 85136)+ , (brightnessValue, ExifRational (-77686) 13317)+ , (exposureBiasValue, ExifRational 5 128)+ , (meteringMode, ExifNumber 5)+ , (flash, ExifNumber 16)+ , (focalLength, ExifRational 111 50)+ , (subjectArea, ExifNumberList [2010,1508,2211,1324])+ , (subSecTimeOriginal, ExifText "458")+ , (subSecTimeDigitized, ExifText "458")+ , (colorSpace, ExifNumber 65535)+ , (exifImageWidth, ExifNumber 4032)+ , (exifImageHeight, ExifNumber 3024)+ , (sensingMethod, ExifNumber 2)+ , (sceneType, ExifUndefined "\SOH")+ , (exposureMode, ExifNumber 0)+ , (whiteBalance, ExifNumber 0)+ , (digitalZoomRatio, ExifRational 126 71)+ , (focalLengthIn35mmFilm, ExifNumber 24)+ , (lensSpecification, ExifRationalList [(111,50),(9,1),(1244236,699009),(14,5)])+ , (lensMake, ExifText "Apple")+ , (lensModel, ExifText "iPhone 14 Pro Max back triple camera 2.22mm f/2.2")+ , (compositeImage, ExifNumber 3)+ , (compositeImageCount, ExifNumberList [13, 0])+ , (make, ExifText "Apple")+ , (model, ExifText "iPhone 14 Pro Max")+ , (orientation, ExifNumber 6)+ , (xResolution, ExifRational 72 1)+ , (yResolution, ExifRational 72 1)+ , (resolutionUnit, ExifNumber 2)+ , (software, ExifText "16.4.1")+ , (dateTime, ExifText "2023:05:07 19:21:26")+ ]