packages feed

exiftool 0.1.1.0 → 0.2.0.0

raw patch · 4 files changed

+229/−226 lines, 4 filesdep ~aesondep ~hashablePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, hashable

API changes (from Hackage documentation)

- ExifTool: (~~) :: Metadata -> Tag -> Metadata
- ExifTool: [tagFamily0] :: Tag -> !Text
- ExifTool: [tagFamily1] :: Tag -> !Text
- ExifTool: data Tag
- ExifTool: deleteMeta :: ExifTool -> [Tag] -> Text -> IO ()
- ExifTool: deleteMetaEither :: ExifTool -> [Tag] -> Text -> IO (Either Text ())
- ExifTool: filterByTag :: (Tag -> Bool) -> Metadata -> Metadata
- ExifTool: getMeta :: ExifTool -> Text -> IO Metadata
- ExifTool: getMetaEither :: ExifTool -> Text -> IO (Either Text Metadata)
- ExifTool: infixl 8 ~~
- ExifTool: instance GHC.Generics.Generic ExifTool.Tag
- ExifTool: setMeta :: ExifTool -> Metadata -> Text -> IO ()
- ExifTool: setMetaEither :: ExifTool -> Metadata -> Text -> IO (Either Text ())
- ExifTool: type Metadata = HashMap Tag Value
+ ExifTool: class FromValue a
+ ExifTool: class ToValue a
+ ExifTool: data Metadata
+ ExifTool: del :: Tag -> Metadata -> Metadata
+ ExifTool: fromValue :: FromValue a => Value -> Maybe a
+ ExifTool: get :: FromValue a => Tag -> Metadata -> Maybe a
+ ExifTool: instance ExifTool.FromValue Data.ByteString.Internal.ByteString
+ ExifTool: instance ExifTool.FromValue Data.Text.Internal.Text
+ ExifTool: instance ExifTool.FromValue ExifTool.Value
+ ExifTool: instance ExifTool.FromValue GHC.Integer.Type.Integer
+ ExifTool: instance ExifTool.FromValue GHC.Types.Bool
+ ExifTool: instance ExifTool.FromValue GHC.Types.Double
+ ExifTool: instance ExifTool.FromValue GHC.Types.Float
+ ExifTool: instance ExifTool.FromValue GHC.Types.Int
+ ExifTool: instance ExifTool.FromValue a => ExifTool.FromValue [a]
+ ExifTool: instance ExifTool.ToValue Data.ByteString.Internal.ByteString
+ ExifTool: instance ExifTool.ToValue Data.Text.Internal.Text
+ ExifTool: instance ExifTool.ToValue ExifTool.Value
+ ExifTool: instance ExifTool.ToValue GHC.Integer.Type.Integer
+ ExifTool: instance ExifTool.ToValue GHC.Types.Bool
+ ExifTool: instance ExifTool.ToValue GHC.Types.Double
+ ExifTool: instance ExifTool.ToValue GHC.Types.Float
+ ExifTool: instance ExifTool.ToValue GHC.Types.Int
+ ExifTool: instance ExifTool.ToValue a => ExifTool.ToValue [a]
+ ExifTool: instance GHC.Base.Monoid ExifTool.Metadata
+ ExifTool: instance GHC.Base.Semigroup ExifTool.Metadata
+ ExifTool: instance GHC.Classes.Eq ExifTool.Metadata
+ ExifTool: instance GHC.Show.Show ExifTool.Metadata
+ ExifTool: newtype Tag
+ ExifTool: readMeta :: ExifTool -> [Tag] -> FilePath -> IO Metadata
+ ExifTool: readMetaEither :: ExifTool -> [Tag] -> FilePath -> IO (Either Text Metadata)
+ ExifTool: set :: ToValue a => Tag -> a -> Metadata -> Metadata
+ ExifTool: stripGroups :: Tag -> Tag
+ ExifTool: toValue :: ToValue a => a -> Value
+ ExifTool: writeMeta :: ExifTool -> Metadata -> FilePath -> IO ()
+ ExifTool: writeMetaEither :: ExifTool -> Metadata -> FilePath -> IO (Either Text ())
- ExifTool: Tag :: !Text -> !Text -> !Text -> Tag
+ ExifTool: Tag :: Text -> Tag
- ExifTool: [tagName] :: Tag -> !Text
+ ExifTool: [tagName] :: Tag -> Text

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Revision history for exiftool +## 0.2.0.0 -- 2021-09-28++* New (backwards-incompatible) API. Highlights:+  * The simplified Tag type allows greater syntactic flexibility when specifying+    tag names.+  * The new FromValue/ToValue type classes and new functions for querying and+    manipulating metadata (get/set/del) support polymorphic values.+ ## 0.1.1.0 -- 2021-04-24  * The getMeta* functions now extract unknown tags as well (exiftool -U option).
README.md view
@@ -11,21 +11,21 @@ A short code example:  ```haskell-{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} -import Data.HashMap.Strict ((!?))+import Data.Text (Text) import ExifTool -example :: IO ()-example =-  withExifTool $ \et -> do-    -- Read metadata, with exact (!?) and fuzzy (~~) tag lookup.-    m <- getMeta et "a.jpg"-    print $ m !? Tag "EXIF" "ExifIFD" "DateTimeOriginal"-    print $ m ~~ Tag "EXIF" "" "XResolution"-    print $ m ~~ Tag "XMP" "" ""-    -- Write and delete metadata.-    setMeta et [(Tag "XMP" "XMP-dc" "Description", String "...")] "a.jpg"-    deleteMeta et [Tag "XMP" "XMP-dc" "Description"] "a.jpg"+data Foo = Foo+  { description :: Text,+    resolution :: Int+  }+  deriving (Show)++main :: IO ()+main = withExifTool $ \et -> do+  m <- readMeta et [] "a.jpg"+  print $ Foo <$> get (Tag "Description") m <*> get (Tag "XResolution") m+  let m' = del (Tag "Description") . set (Tag "XResolution") (42 :: Int) $ m+  writeMeta et m' "a.jpg" ```
exiftool.cabal view
@@ -1,5 +1,5 @@ name:                 exiftool-version:              0.1.1.0+version:              0.2.0.0 synopsis:             Haskell bindings to ExifTool description:          Haskell bindings to the [ExifTool](https://exiftool.org)                       command-line application that enable reading, writing and
src/ExifTool.hs view
@@ -1,11 +1,10 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-}  -- | -- Module     : ExifTool--- Copyright  : (c) Martin Hoppenheit 2021+-- Copyright  : (c) Martin Hoppenheit 2020-2021 -- License    : MIT -- Maintainer : martin@hoppenheit.info --@@ -14,23 +13,23 @@ -- in various file formats. Here's a short code example, the details are -- explained below. ----- > {-# LANGUAGE OverloadedLists #-} -- > {-# LANGUAGE OverloadedStrings #-} -- > --- > import Data.HashMap.Strict ((!?))+-- > import Data.Text (Text) -- > import ExifTool -- > --- > example :: IO ()--- > example =--- >   withExifTool $ \et -> do--- >     -- Read metadata, with exact (!?) and fuzzy (~~) tag lookup.--- >     m <- getMeta et "a.jpg"--- >     print $ m !? Tag "EXIF" "ExifIFD" "DateTimeOriginal"--- >     print $ m ~~ Tag "EXIF" "" "XResolution"--- >     print $ m ~~ Tag "XMP" "" ""--- >     -- Write and delete metadata.--- >     setMeta et [(Tag "XMP" "XMP-dc" "Description", String "...")] "a.jpg"--- >     deleteMeta et [Tag "XMP" "XMP-dc" "Description"] "a.jpg"+-- > data Foo = Foo+-- >   { description :: Text,+-- >     resolution :: Int+-- >   }+-- >   deriving (Show)+-- > +-- > main :: IO ()+-- > main = withExifTool $ \et -> do+-- >   m <- readMeta et [] "a.jpg"+-- >   print $ Foo <$> get (Tag "Description") m <*> get (Tag "XResolution") m+-- >   let m' = del (Tag "Description") . set (Tag "XResolution") (42 :: Int) $ m+-- >   writeMeta et m' "a.jpg" -- -- Note that this module expects the @exiftool@ binary to be in your PATH. @@ -47,46 +46,36 @@     withExifTool,     -- * Reading and writing metadata     ---    -- | The ExifTool instance can then be used to read, write or delete-    -- metadata in a file with the respective functions. These come in two-    -- variants, one that throws runtime errors when the ExifTool process-    -- returns error messages and one that instead produces Either values.-    -- Choose those that best fit your use case.-    getMeta,-    setMeta,-    deleteMeta,-    getMetaEither,-    setMetaEither,-    deleteMetaEither,-    -- * Data types and utility functions-    ---    -- | Metadata is represented by a 'Data.HashMap.Strict.HashMap' of-    -- 'Tag'/'Value' pairs (with alias 'Metadata'), so it is advisable to-    -- import some functions like 'Data.HashMap.Strict.lookup' or-    -- 'Data.HashMap.Strict.!?' from the "Data.HashMap.Strict" module. The-    -- ExifTool module defines additional utility functions that make working-    -- with Metadata easier.+    -- | The ExifTool instance can then be used to read or write metadata in a+    -- file with the respective functions.+    readMeta,+    readMetaEither,+    writeMeta,+    writeMetaEither,+    -- | Metadata is represented by a set of 'Tag'/'Value' pairs that can be+    -- queried and manipulated with the respective functions.     Metadata,     Tag (..),+    stripGroups,     Value (..),-    filterByTag,-    (~~),+    FromValue (..),+    ToValue (..),+    get,+    set,+    del,   ) where  import Control.Exception (bracket)-import Control.Monad (void)+import Control.Monad (guard, void) import Data.Bifunctor (bimap)-import GHC.Generics (Generic) import System.IO (Handle, hFlush, hReady)  import Data.Aeson   ( FromJSON (..),     FromJSONKey (..),-    FromJSONKeyFunction (..),     ToJSON (..),     ToJSONKey (..),-    ToJSONKeyFunction (..),     eitherDecode,     encode,   )@@ -95,12 +84,27 @@ import Data.ByteString (ByteString) import Data.ByteString.Base64 (decodeBase64, encodeBase64) import Data.ByteString.Lazy (hPut)-import Data.HashMap.Strict (HashMap, delete, filterWithKey, fromList)+import Data.HashMap.Strict (HashMap, delete, insert, mapKeys, (!?)) import Data.Hashable (Hashable)-import Data.Scientific (Scientific)+import Data.Scientific+  ( FPFormat (Fixed),+    Scientific,+    formatScientific,+    fromFloatDigits,+    isInteger,+    toBoundedInteger,+    toRealFloat,+  ) import Data.String.Conversions (cs)-import Data.Text (Text)-import qualified Data.Text as T+import Data.Text+  ( Text,+    intercalate,+    isPrefixOf,+    splitOn,+    stripPrefix,+    toCaseFold,+  )+import Data.Text.Encoding (decodeUtf8') import Data.Text.IO (hGetLine, hPutStrLn) import qualified Data.Vector as Vector import System.IO.Temp (withSystemTempFile)@@ -128,64 +132,29 @@       -- process handle of this ExifTool process       !ProcessHandle --- | A set of ExifTool tag/value pairs.-type Metadata = HashMap Tag Value---- | An ExifTool tag name, consisting of three components:------ 1. The family 0 tag group (information type) e.g., @EXIF@ or @XMP@.--- 2. The family 1 tag group (specific location) e.g., @IFD0@ or @XMP-dc@.--- 3. The actual tag name e.g., @XResolution@ or @Description@.------ Example: @Tag \"EXIF\" \"IFD0\" \"XResolution\"@ corresponds to the ExifTool--- tag name @EXIF:IFD0:XResolution@.------ During development, there are several ways to find the exact name of a tag:------ * See <https://exiftool.org/#groups> for a list of tag groups.--- * Run something like @exiftool -s -a -G:0:1@.--- * Use the '~~' operator in ghci.-data Tag = Tag-  { -- | family 0 tag group-    tagFamily0 :: !Text,-    -- | family 1 tag group-    tagFamily1 :: !Text,-    -- | actual tag name-    tagName :: !Text-  }-  deriving (Show, Eq, Generic, Hashable)--instance FromJSON Tag where-  parseJSON (JSON.String x)-    | Just t <- readTag x = return t-  parseJSON x = fail $ "unexpected formatting of ExifTool tag: " <> show x--instance FromJSONKey Tag where-  fromJSONKey = FromJSONKeyTextParser $ parseJSON . JSON.String--instance ToJSON Tag where-  toJSON = JSON.String . showTag-  toEncoding = text . showTag+-- | A set of ExifTool tag/value pairs. Use 'get', 'set' and 'del' to query and+-- manipulate this set.+newtype Metadata = Metadata (HashMap Tag Value)+  deriving (Show, Eq)+  deriving (Semigroup, Monoid) via (HashMap Tag Value) -instance ToJSONKey Tag where-  toJSONKey = ToJSONKeyText showTag (text . showTag)+-- | An ExifTool tag name like @Tag "Description"@ or @Tag+-- "EXIF:IFD0:XResolution"@.+newtype Tag = Tag {tagName :: Text}+  deriving (Show, Eq)+  deriving (Hashable, FromJSON, FromJSONKey, ToJSON, ToJSONKey) via Text --- | Parse an ExifTool tag name of the form @family0:family1:name@,--- @family0:name@ or @name@ (but /not/ @family1:name@).-readTag :: Text -> Maybe Tag-readTag t =-  case T.splitOn ":" t of-    [f0, f1, n] -> Just $ Tag f0 f1 n-    [f0, n] -> Just $ Tag f0 "" n-    [n] -> Just $ Tag "" "" n-    _ -> Nothing+-- | Remove group prefixes from a tag name e.g., @stripGroups (Tag+-- "XMP:XMP-dc:Description") == Tag "Description"@.+stripGroups :: Tag -> Tag+stripGroups = Tag . last . splitOn ":" . tagName --- | Format an ExifTool tag name in the form @family0:family1:name@,--- @family0:name@, @family1:name@ or @name@.-showTag :: Tag -> Text-showTag (Tag f0 f1 n) = T.intercalate ":" $ filter (not . T.null) [f0, f1, n]+-- | Make a tag name lower case.+toLower :: Tag -> Tag+toLower = Tag . toCaseFold . tagName --- | An ExifTool tag value, enclosed in a type wrapper.+-- | An ExifTool tag value, enclosed in a type wrapper. The type wrapper can+-- usually be ignored when using the 'FromValue' and 'ToValue' instances. data Value   = String !Text   | Binary !ByteString@@ -197,14 +166,13 @@  instance FromJSON Value where   parseJSON (JSON.String x)-    | Just b <- T.stripPrefix "base64:" x =-      either (fail . cs) (return . Binary) (decodeBase64 $ cs b)-    | otherwise = return $ String x-  parseJSON (JSON.Number x) = return $ Number x-  parseJSON (JSON.Bool x) = return $ Bool x-  parseJSON (JSON.Array xs) =-    List <$> sequence (Vector.toList $ fmap parseJSON xs)-  parseJSON JSON.Null = return $ String ""+    | Just b <- stripPrefix "base64:" x =+      either (fail . cs) (pure . Binary) (decodeBase64 $ cs b)+    | otherwise = pure $ String x+  parseJSON (JSON.Number x) = pure $ Number x+  parseJSON (JSON.Bool x) = pure $ Bool x+  parseJSON (JSON.Array xs) = List . Vector.toList <$> traverse parseJSON xs+  parseJSON JSON.Null = pure $ String ""   -- parseJSON (JSON.Object x) = Struct <$> sequence (fmap parseJSON x)   parseJSON x = fail $ "error parsing ExifTool JSON output: " <> show x @@ -220,12 +188,89 @@   toEncoding (Bool x) = bool x   toEncoding (List xs) = list toEncoding xs +-- | Data types that a 'Value' can be turned into.+--+-- @since 0.2.0.0+class FromValue a where+  fromValue :: Value -> Maybe a++-- | Data types that can be turned into a 'Value'.+--+-- @since 0.2.0.0+class ToValue a where+  toValue :: a -> Value++instance FromValue Value where+  fromValue = Just++instance ToValue Value where+  toValue = id++instance FromValue Text where+  fromValue (String x) = Just x+  fromValue (Binary x) = either (const Nothing) Just $ decodeUtf8' x+  fromValue (Number x) =+    Just . cs . formatScientific Fixed (Just $ if isInteger x then 0 else 2) $ x+  fromValue (Bool x) = Just . cs . show $ x+  fromValue (List xs) = intercalate ", " <$> traverse fromValue xs++instance ToValue Text where+  toValue = String++instance FromValue ByteString where+  fromValue (Binary x) = Just x+  fromValue _ = Nothing++instance ToValue ByteString where+  toValue = Binary++instance FromValue Int where+  fromValue (Number x) = toBoundedInteger x+  fromValue _ = Nothing++instance ToValue Int where+  toValue = Number . fromIntegral++instance FromValue Integer where+  fromValue x = fromIntegral <$> (fromValue x :: Maybe Int)++instance ToValue Integer where+  toValue = Number . fromIntegral++instance FromValue Float where+  fromValue (Number x) = Just $ toRealFloat x+  fromValue _ = Nothing++instance ToValue Float where+  toValue = Number . fromFloatDigits++instance FromValue Double where+  fromValue (Number x) = Just $ toRealFloat x+  fromValue _ = Nothing++instance ToValue Double where+  toValue = Number . fromFloatDigits++instance FromValue Bool where+  fromValue (Bool x) = Just x+  fromValue _ = Nothing++instance ToValue Bool where+  toValue = Bool++instance FromValue a => FromValue [a] where+  fromValue (List xs) = traverse fromValue xs+  fromValue _ = Nothing++instance ToValue a => ToValue [a] where+  toValue = List . fmap toValue+ -- | Start an ExifTool instance. Use 'stopExifTool' when done, or 'withExifTool' -- to combine both steps. startExifTool :: IO ExifTool startExifTool = do   (Just i, Just o, Just e, p) <- createProcess conf-  return $ ET i o e p+  pure $ ET i o e p   where     conf =       (proc "exiftool" options)@@ -259,7 +304,7 @@   -- Do not switch the order of readOut/readErr lest we miss errors!   out <- readOut o ""   err <- readErr e ""-  return $+  pure $     if isError err       then Left err       else Right out@@ -267,133 +312,83 @@     readOut :: Handle -> Text -> IO Text     readOut h acc = do       l <- hGetLine h-      if "{ready}" `T.isPrefixOf` l-        then return acc+      if "{ready}" `isPrefixOf` l+        then pure acc         else readOut h (acc <> l)     readErr :: Handle -> Text -> IO Text     readErr h acc = do       hasMore <- hReady h       if not hasMore-        then return acc+        then pure acc         else do           l <- hGetLine h           readErr h (acc <> l)     isError :: Text -> Bool     isError t = t `notElem` ["", "    1 image files updated"] --- | Read all metadata from a file, with ExifTool errors leading to runtime--- errors. (Use 'getMetaEither' instead if you would rather intercept them.)-getMeta ::-  -- | ExifTool instance-  ExifTool ->-  -- | file name-  Text ->-  -- | tag/value Map-  IO Metadata-getMeta et file = eitherError <$> getMetaEither et file+-- | Read the given tags from a file. Use an empty tag list to return all+-- metadata. Tag names are returned in "simple" form without any leading group+-- prefixes, independent of how they are specified in the given tag list.+--+-- @since 0.2.0.0+readMeta :: ExifTool -> [Tag] -> FilePath -> IO Metadata+readMeta et ts fp = eitherError <$> readMetaEither et ts fp --- | Read all metadata from a file, with ExifTool errors returned as Left--- values.-getMetaEither ::-  -- | ExifTool instance-  ExifTool ->-  -- | file name-  Text ->-  -- | tag/value Map-  IO (Either Text Metadata)-getMetaEither et file = do-  result <- sendCommand et (file : options)-  return $ result >>= parseOutput+-- | Like 'readMeta', but ExifTool errors are returned as Left values instead of+-- leading to runtime errors.+--+-- @since 0.2.0.0+readMetaEither :: ExifTool -> [Tag] -> FilePath -> IO (Either Text Metadata)+readMetaEither et ts fp = do+  result <- sendCommand et (cs fp : options <> tags)+  pure $ Metadata . mapKeys toLower <$> (result >>= parseOutput)   where-    parseOutput :: Text -> Either Text Metadata+    options = ["-json", "-binary", "-unknown2"]+    tags = fmap (("-" <>) . tagName) ts     parseOutput = bimap cs head . eitherDecode . cs-    options = ["-json", "-a", "-U", "-G:0:1", "-s", "-binary"] --- | Write metadata to a file, with ExifTool errors leading to runtime errors.--- (Use 'setMetaEither' instead if you would rather intercept them.) The file is--- modified in place. Make sure you have the necessary backups!-setMeta ::-  -- | ExifTool instance-  ExifTool ->-  -- | tag/value Map-  Metadata ->-  -- | file name-  Text ->-  IO ()-setMeta et m file = eitherError <$> setMetaEither et m file+-- | Write metadata to a file. The file is modified in place, make sure you have+-- the necessary backups!+--+-- @since 0.2.0.0+writeMeta :: ExifTool -> Metadata -> FilePath -> IO ()+writeMeta et m fp = eitherError <$> writeMetaEither et m fp --- | Write metadata to a file, with ExifTool errors returned as Left values. The--- file is modified in place. Make sure you have the necessary backups!-setMetaEither ::-  -- | ExifTool instance-  ExifTool ->-  -- | tag/value Map-  Metadata ->-  -- | file name-  Text ->-  IO (Either Text ())-setMetaEither et m file =+-- | Like 'writeMeta', but ExifTool errors are returned as Left values instead+-- of leading to runtime errors.+--+-- @since 0.2.0.0+writeMetaEither :: ExifTool -> Metadata -> FilePath -> IO (Either Text ())+writeMetaEither et (Metadata m) fp =   withSystemTempFile "exiftool.json" $ \metafile h -> do-    hPut h $ encode [delete (Tag "" "" "SourceFile") m]+    hPut h $ encode [delete (Tag "SourceFile") m]     hFlush h-    void <$> sendCommand et (file : "-json=" <> cs metafile : options)+    void <$> sendCommand et (cs fp : "-json=" <> cs metafile : options)   where     options = ["-overwrite_original", "-f"] --- | Delete metadata from a file, with ExifTool errors leading to runtime--- errors. (Use 'deleteMetaEither' instead if you would rather intercept them.)--- The file is modified in place. Make sure you have the necessary backups!-deleteMeta ::-  -- | ExifTool instance-  ExifTool ->-  -- | tags to be deleted-  [Tag] ->-  -- | file name-  Text ->-  IO ()-deleteMeta et ts file = eitherError <$> deleteMetaEither et ts file---- | Delete metadata from a file, with ExifTool errors returned as Left values.--- The file is modified in place. Make sure you have the necessary backups!-deleteMetaEither ::-  -- | ExifTool instance-  ExifTool ->-  -- | tags to be deleted-  [Tag] ->-  -- | file name-  Text ->-  IO (Either Text ())-deleteMetaEither et ts = setMetaEither et (fromList $ fmap (,String "-") ts)---- | Filter metadata by tag name.-filterByTag :: (Tag -> Bool) -> Metadata -> Metadata-filterByTag p = filterWithKey (\t _ -> p t)---- | Filter metadata by fuzzy tag name matching. Tag names are matched ignoring--- case, and empty components of the given tag name are considered wildcards.--- Examples:+-- | Retrieve the value of a tag. Tag case is ignored i.e., @get (Tag+-- "Description)" m == get (Tag "description") m@. ----- * @m ~~ Tag \"EXIF\" \"IFD0\" \"XResolution\"@ matches exactly the given tag---   name (ignoring case)--- * @m ~~ Tag "exif" "" "xresolution"@ matches all EXIF tags with name---   xresolution (ignoring case), including @EXIF:IFD0:XResolution@ and---   @EXIF:IFD1:XResolution@--- * @m ~~ Tag \"XMP\" "" ""@ matches all XMP tags+-- @since 0.2.0.0+get :: FromValue a => Tag -> Metadata -> Maybe a+get t (Metadata m) = do+  v <- m !? toLower t+  guard (v /= String "-") -- Marked for deletion, see del function below.+  fromValue v++-- | Set a tag to a (new) value. Tag case is ignored. ----- Note that @~~@ has higher precedence than '<>', so @m ~~ t <> m ~~ t' == (m--- ~~ t) <> (m ~~ t')@ which makes combining filters easy.+-- @since 0.2.0.0+set :: ToValue a => Tag -> a -> Metadata -> Metadata+set t v (Metadata m) = Metadata $ insert (toLower t) (toValue v) m++-- | Delete a tag (i.e., set its value to a marker that will make ExifTool+-- delete it when 'writeMeta' is called). Tag case is ignored. ----- Hint: This operator is useful to find exact tag names in ghci.-infixl 8 ~~-(~~) :: Metadata -> Tag -> Metadata-(~~) m t = filterByTag (match t) m-  where-    match :: Tag -> Tag -> Bool-    match (Tag f0 f1 n) (Tag f0' f1' n') =-      match' f0 f0' && match' f1 f1' && match' n n'-    match' :: Text -> Text -> Bool-    match' "" _ = True -- But not in reverse!-    match' x x' = T.toCaseFold x == T.toCaseFold x'+-- @since 0.2.0.0+del :: Tag -> Metadata -> Metadata+del t = set t (String "-")  -- | Extract content from Right or throw error. eitherError :: Either Text a -> a