packages feed

hsexif 0.6.1.6 → 0.6.1.7

raw patch · 6 files changed

+74/−96 lines, 6 filesdep ~basebinary-addedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base

API changes (from Hackage documentation)

- Graphics.HsExif: exifIfdOffset :: ExifTag
- Graphics.HsExif: gpsTagOffset :: ExifTag
- Graphics.HsExif: instance GHC.Show.Show Graphics.HsExif.IfEntry
- Graphics.HsExif: ExifTag :: TagLocation -> Maybe String -> Word16 -> ExifValue -> Text -> ExifTag
+ Graphics.HsExif: ExifTag :: TagLocation -> Maybe String -> Word16 -> (ExifValue -> Text) -> ExifTag

Files

Graphics/ExifTags.hs view
@@ -109,8 +109,6 @@ yCbCrPositioning        = exifIfd0Tag "yCbCrPositioning" 0x0213 ppYCbCrPositioning referenceBlackWhite     = exifIfd0Tag "referenceBlackWhite" 0x0214 showT copyright               = exifIfd0Tag "copyright" 0x8298 showT-exifIfdOffset           = exifIfd0Tag "exifIfdOffset" 0x8769 showT-gpsTagOffset            = exifIfd0Tag "gpsTagOffset" 0x8825 showT printImageMatching      = exifIfd0Tag "printImageMatching" 0xc4a5 ppUndef  gpsVersionID            = exifGpsTag "gpsVersionID" 0x0000 showT@@ -163,7 +161,7 @@     imageUniqueId, exifInteroperabilityOffset, imageDescription,     xResolution, yResolution, resolutionUnit, dateTime, whitePoint,     primaryChromaticities, yCbCrPositioning, yCbCrCoefficients, referenceBlackWhite,-    exifIfdOffset, printImageMatching, gpsTagOffset, artist,+    printImageMatching, artist,     gpsVersionID, gpsLatitudeRef, gpsLatitude, gpsLongitudeRef, gpsLongitude,     gpsAltitudeRef, gpsAltitude, gpsTimeStamp, gpsSatellites, gpsStatus,     gpsMeasureMode, gpsDop, gpsSpeedRef, gpsSpeed, gpsTrackRef, gpsTrack,
Graphics/HsExif.hs view
@@ -135,9 +135,7 @@     yCbCrPositioning,     yCbCrCoefficients,     referenceBlackWhite,-    exifIfdOffset,     printImageMatching,-    gpsTagOffset,      -- * If you need to declare your own exif tags     ExifTag(..),@@ -205,7 +203,10 @@     markerNumber <- getWord16be     dataSize <- fromIntegral . toInteger <$> getWord16be     case markerNumber of-        0xffe1 -> parseExifBlock+        0xffe1 -> tryParseExifBlock >>= \case+          Right exif -> pure exif+          -- try will fail for XMP content for instance+          Left bytesReadByTry -> skip (dataSize - 2 - bytesReadByTry) >> findAndParseExifBlockJPEG         -- ffda is Start Of Stream => image         -- I expect no more EXIF data after this point.         0xffda -> fail "No EXIF in JPEG"@@ -244,32 +245,22 @@ putWord32 Intel = putWord32le putWord32 Motorola = putWord32be -parseExifBlock :: Get (Map ExifTag ExifValue)-parseExifBlock = do+-- return either the exif info, or the number of bytes read.+tryParseExifBlock :: Get (Either Int (Map ExifTag ExifValue))+tryParseExifBlock = do     header <- getByteString 4     nul <- toInteger <$> getWord16be-    unless (header == Char8.pack "Exif" && nul == 0)-        $ fail "invalid EXIF header"-    parseTiff+    if header == Char8.pack "Exif" && nul == 0+      then Right <$> parseTiff+      else pure (Left 6) -- read 6 bytes: 4+2  parseTiff :: Get (Map ExifTag ExifValue) parseTiff = do-    tiffHeaderStart <- fromIntegral <$> bytesRead-    byteAlign <- parseTiffHeader-    let subIfdParse = parseSubIFD byteAlign tiffHeaderStart-    (mExifSubIfdOffsetW, mGpsOffsetW, ifdEntries) <- parseIfd byteAlign tiffHeaderStart-    gpsData <- maybe (return []) (lookAhead . subIfdParse GpsSubIFD) mGpsOffsetW-    exifSubEntries <- maybe (return []) (subIfdParse ExifSubIFD) mExifSubIfdOffsetW-    return $ Map.fromList $ ifdEntries ++ exifSubEntries ++ gpsData--parseSubIFD :: ByteAlign -> Int -> TagLocation -> Word32 -> Get [(ExifTag, ExifValue)]-parseSubIFD byteAlign tiffHeaderStart ifdType offsetW = do-    let offset = fromIntegral $ toInteger offsetW-    bytesReadNow <- fromIntegral <$> bytesRead-    skip $ (offset + tiffHeaderStart) - bytesReadNow-    parseSubIfd byteAlign tiffHeaderStart ifdType+    (byteAlign, ifdOffset) <- lookAhead parseTiffHeader+    tags <- parseIfd byteAlign IFD0 ifdOffset+    return $ Map.fromList tags -parseTiffHeader :: Get ByteAlign+parseTiffHeader :: Get (ByteAlign, Int) parseTiffHeader = do     byteAlignV <- Char8.unpack <$> getByteString 2     byteAlign <- case byteAlignV of@@ -280,49 +271,38 @@     unless (alignControl == 0x2a || (byteAlign == Intel && (alignControl == 0x55 || alignControl == 0x4f52)))         $ fail "exif byte alignment mismatch"     ifdOffset <- fromIntegral . toInteger <$> getWord32 byteAlign-    skip $ ifdOffset - 8-    return byteAlign--parseIfd :: ByteAlign -> Int -> Get (Maybe Word32, Maybe Word32, [(ExifTag, ExifValue)])-parseIfd byteAlign tiffHeaderStart = do-    dirEntriesCount <- fromIntegral <$> getWord16 byteAlign-    ifdEntries <- replicateM dirEntriesCount (parseIfEntry byteAlign)-    let exifOffset = entryContentsByTag exifIfdOffset ifdEntries-    let gpsOffset = entryContentsByTag gpsTagOffset ifdEntries-    entries <- mapM (decodeEntry byteAlign tiffHeaderStart IFD0) ifdEntries-    return (exifOffset, gpsOffset, entries)+    return (byteAlign, ifdOffset) -entryContentsByTag :: ExifTag -> [IfEntry] -> Maybe Word32-entryContentsByTag tag = fmap entryContents . find (\e -> entryTag e == tagKey tag)+-- | Parse an Image File Directory table+parseIfd :: ByteAlign -> TagLocation -> Int -> Get [(ExifTag, ExifValue)]+parseIfd byteAlign ifdId offset = do+    entries <- lookAhead $ do+        skip offset+        dirEntriesCount <- fromIntegral <$> getWord16 byteAlign+        replicateM dirEntriesCount (parseIfEntry byteAlign ifdId)+    +    concat <$> mapM (entryTags byteAlign) entries -parseSubIfd :: ByteAlign -> Int -> TagLocation -> Get [(ExifTag, ExifValue)]-parseSubIfd byteAlign tiffHeaderStart location = do-    dirEntriesCount <- fromIntegral <$> getWord16 byteAlign-    ifdEntries <- replicateM dirEntriesCount (parseIfEntry byteAlign)-    mapM (decodeEntry byteAlign tiffHeaderStart location) ifdEntries+-- | Convert IFD entries to tags, reading sub-IFDs+entryTags :: ByteAlign -> IfEntry -> Get [(ExifTag, ExifValue)]+entryTags _ (Tag tag parseValue) = parseValue >>= \value -> pure [(tag, value)]+entryTags byteAlign (SubIFD ifdId offset) = lookAhead (parseIfd byteAlign ifdId offset) -data IfEntry = IfEntry-    {-        entryTag :: !Word16,-        entryFormat :: !Word16,-        entryNoComponents :: !Int,-        entryContents :: !Word32-    } deriving Show+data IfEntry = Tag ExifTag (Get ExifValue) | SubIFD TagLocation Int -parseIfEntry :: ByteAlign -> Get IfEntry-parseIfEntry byteAlign = do+-- | Parse a single IFD entry+parseIfEntry :: ByteAlign -> TagLocation -> Get IfEntry+parseIfEntry byteAlign ifdId = do     tagNumber <- getWord16 byteAlign-    dataFormat <- getWord16 byteAlign-    numComponents <- getWord32 byteAlign-    value <- getWord32 byteAlign-    return IfEntry-        {-            entryTag = tagNumber,-            entryFormat = dataFormat,-            entryNoComponents = fromIntegral $ toInteger numComponents,-            entryContents = value-        }+    format <- getWord16 byteAlign+    numComponents <- fromIntegral <$> getWord32 byteAlign+    content <- getWord32 byteAlign +    return $ case (ifdId, tagNumber) of+        (IFD0, 0x8769) -> SubIFD ExifSubIFD (fromIntegral content)+        (IFD0, 0x8825) -> SubIFD GpsSubIFD  (fromIntegral content)+        (_, tagId)     -> Tag (getExifTag ifdId tagId) (decodeEntry byteAlign format numComponents content)+ getExifTag :: TagLocation -> Word16 -> ExifTag getExifTag l v = fromMaybe (ExifTag l Nothing v showT) $ find (isSameTag l v) allExifTags     where isSameTag l1 v1 (ExifTag l2 _ v2 _) = l1 == l2 && v1 == v2@@ -455,49 +435,36 @@         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-    -- 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 getHandler $ entryFormat entry of-            Just handler -> decodeEntryWithHandler byteAlign tiffHeaderStart handler entry-            Nothing -> return $ ExifUnknown (entryFormat entry) (entryNoComponents entry) contentsInt-    return (exifTag, tagValue)+decodeEntry :: ByteAlign -> Word16 -> Int -> Word32 -> Get ExifValue+decodeEntry byteAlign format amount payload = do+    case getHandler format of+        Just handler | isInline handler -> return $ parseInline byteAlign handler amount (runPut $ putWord32 byteAlign payload)+        Just handler                    -> parseOffset byteAlign handler amount payload+        Nothing -> return $ ExifUnknown format amount (fromIntegral payload)+    where+        isInline handler = dataLength handler * amount <= 4  getHandler :: Word16 -> Maybe ValueHandler getHandler typeId = find ((==typeId) . dataTypeId) valueHandlers -decodeEntryWithHandler :: ByteAlign -> Int -> ValueHandler -> IfEntry -> Get ExifValue-decodeEntryWithHandler byteAlign tiffHeaderStart handler entry =-    if dataLength handler * entryNoComponents entry <= 4-        then do-            let inlineBs = runPut $ putWord32 byteAlign $ entryContents entry-            return $ parseInline byteAlign handler entry inlineBs-        else parseOffset byteAlign tiffHeaderStart handler entry--parseInline :: ByteAlign -> ValueHandler -> IfEntry -> B.ByteString -> ExifValue-parseInline byteAlign handler entry bytestring =+parseInline :: ByteAlign -> ValueHandler -> Int -> B.ByteString -> ExifValue+parseInline byteAlign handler amount bytestring =     fromJust $ runMaybeGet getter bytestring     where-        getter = case entryNoComponents entry of+        getter = case amount of             1 -> readSingle handler byteAlign-            _ -> readMany handler byteAlign $ entryNoComponents entry+            _ -> readMany handler byteAlign amount -parseOffset :: ByteAlign -> Int -> ValueHandler -> IfEntry -> Get ExifValue-parseOffset byteAlign tiffHeaderStart handler entry = do-    let contentsInt = fromIntegral $ toInteger $ entryContents entry-    curPos <- fromIntegral <$> bytesRead+parseOffset :: ByteAlign -> ValueHandler -> Int -> Word32 -> Get ExifValue+parseOffset byteAlign handler amount offset = do     -- this skip can take me quite far and I can't skip     -- back with binary. So do the skip with a lookAhead.     -- see https://github.com/emmanueltouzery/hsexif/issues/9     lookAhead $ do-        skip (contentsInt + tiffHeaderStart - curPos)-        let bsLength = entryNoComponents entry * dataLength handler+        skip (fromIntegral offset)+        let bsLength = amount * dataLength handler         bytestring <- getLazyByteString (fromIntegral bsLength)-        return (parseInline byteAlign handler entry bytestring)+        return (parseInline byteAlign handler amount bytestring)  signedInt32ToInt :: Word32 -> Int signedInt32ToInt w = fromIntegral (fromIntegral w :: Int32)
hsexif.cabal view
@@ -1,5 +1,5 @@ name:                hsexif-version:             0.6.1.6+version:             0.6.1.7 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
@@ -55,6 +55,21 @@         describe "partial exif data" $ testPartialExif partial         describe "tiff file" $ testNef tiffExifData +        describe "unusual data layouts" $ do+            it "parses EXIF below IDF0" $ do+                result <- parseFileExif "tests/test-exif-below-idf0.jpg"+                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")+            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")++ testNotAJpeg :: B.ByteString -> Spec testNotAJpeg imageContents = it "returns empty list if not a JPEG" $     assertEqual' (Left "Not a JPEG, TIFF, RAF, or TIFF-based raw file") (parseExif imageContents)@@ -112,7 +127,6 @@                 (dateTime, ExifText "2014:04:10 20:14:20"),                 (yCbCrPositioning, ExifNumber 2),                 (fileSource, ExifUndefined "\ETX"),-                (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\NUL")             ]) (sort cleanedParsed)     -- the sony maker note is 35k!! Just test its size and that it starts with "SONY DSC".@@ -276,7 +290,6 @@                 (ExifTag IFD0 Nothing 0x14a (T.pack . show), ExifNumber 58880),                 (referenceBlackWhite, ExifRationalList [(0,1),(255,1),(0,1),(255,1),(0,1),(255,1)]),                 (copyright, ExifText "Copyright,NIKON CORPORATION,1999\0"),-                (exifIfdOffset, ExifNumber 528),                 (ExifTag IFD0 Nothing 0x9003 (T.pack . show), ExifText "2000:11:19 13:01:50"),                 (ExifTag IFD0 Nothing 0x9216 (T.pack . show), ExifNumberList [1,0,0,0])               ])
+ tests/test-exif-below-idf0.jpg view

binary file changed (absent → 38580 bytes)

+ tests/xmp-before-exif-truncated.jpg view

binary file changed (absent → 8192 bytes)