11defmodule Memorable.Data.Image do
2+ @ moduledoc """
3+ Functions for working with images in memorable.
4+
5+ An `Image` represents one image stored in a memorable `Collection`. Images are always associated with exactly one
6+ collection - it is not possible for one image to be a part of two different collections.
7+
8+ ## Structure
9+ The `Image` struct holds information about an image relevant to storage and relations. It does not contain other
10+ metadata about the image itself, such as when the photo was taken or other data stored in the image's EXIF tags. These
11+ are instead kept in the `Memorable.Data.Image.DerivedMetadata` table.
12+
13+ ## Fields
14+ - `id`: An [ID](`t:Memorable.Util.id/0`) representing the image.
15+ - `collection_id`: The [ID](`t:Memorable.Util.id/0`) of the `Memorable.Data.Collection` the image is a part of.
16+ - `path`: The path to the image file on disk, relative to the folder memorable stores all images in.
17+ - `imported_datetime`: A `DateTime` representing the time at which the image was imported into the memorable database.
18+ """
19+ @ moduledoc since: "1.0.0"
220 @ derive { Inspect , except: [ ] }
321 use Memento.Table , attributes: [ :id , :collection_id , :path , :imported_datetime ]
422
@@ -10,6 +28,15 @@ defmodule Memorable.Data.Image do
1028 }
1129 @ type metadata :: % { String . t ( ) => any ( ) }
1230
31+ @ doc """
32+ Reads metadata from the image.
33+
34+ To get metadata from the image, memorable calls out to `exiftool` installed on the system to obtain all EXIF tags
35+ stored on the image. See `Subprocess`, or the code in `native/subprocess` for details on how this is done.
36+
37+ Returns a map from EXIF tags to their values, for all tags in the image.
38+ """
39+ @ doc since: "1.0.0"
1340 @ spec read_metadata ( t ( ) ) :: { :ok , metadata ( ) } | { :error , any ( ) }
1441 def read_metadata ( % __MODULE__ { path: path } ) do
1542 with { :file_read , { :ok , data } } <- { :file_read , File . read ( path ) } ,
@@ -26,6 +53,32 @@ defmodule Memorable.Data.Image do
2653end
2754
2855defmodule Memorable.Data.Image.DerivedMetadata do
56+ @ moduledoc """
57+ Metadata associated with images.
58+
59+ Where `Memorable.Data.Image` holds information about the image's internal representation within memorable,
60+ `DerivedMetadata` contains information about the image itself. When an image is imported into memorable,
61+ `from_image/1` is called to extract metadata from EXIF tags, which gets stored in the memorable database for quick
62+ access.
63+
64+ ## Fields
65+ - `image_id`: The [ID](`t:Memorable.Util.id/0`) of the `Memorable.Data.Image` this metadata is associated with.
66+ - `file_hash`: A SHA256 hash of the image file this metadata was derived from. This is used to ensure that the
67+ metadata stored in the table is up to date, and matches the image on disk.
68+
69+ The following fields are retrieved from EXIF metadata stored in the image, and may be `nil` if the associated tags are
70+ not present on the image:
71+ - `original_datetime`: A `NaiveDateTime` representing when the image was taken.
72+ - `lens_model`: The lens used to take the photo.
73+ - `body_model`: The camera body used to take the photo.
74+ - `focal_length`: The focal length the image was taken at, in millimetres.
75+ - `aperture`: The aperture the image was taken at, represented as an f-number.
76+ - `exposure_time`: The duration of time the image was exposed for, in seconds expressed as a rational (eg. "1/1250").
77+ - `iso`: The ISO sensitivity the image was taken at.
78+ """
79+ @ moduledoc since: "1.0.0"
80+ alias Memorable.Data.Image.DerivedMetadata
81+ alias Memorable.Data.Image.DerivedMetadata
2982 alias Memorable.Data.Image
3083
3184 # `image_id` is 1:1 with an Image `id`
@@ -54,6 +107,18 @@ defmodule Memorable.Data.Image.DerivedMetadata do
54107 iso: integer ( )
55108 }
56109
110+ @ doc """
111+ Reads and parses metadata for a `Memorable.Data.Image`.
112+
113+ EXIF tags from an image file are retrieved with `Memorable.Data.Image.read_metadata/1`, and then parsed into the
114+ format described above.
115+
116+ Returns:
117+ - `{:ok, metadata}`: When metadata was successfully read and parsed from the image.
118+ - `{:error, {:read_file, error}}`: When an error occured reading the image file from disk.
119+ - `{:error, {:read_metadata, error}}`: When an error occurred while extracting EXIF tags from the image file.
120+ """
121+ @ doc since: "1.0.0"
57122 @ spec from_image ( Image . t ( ) ) :: { :ok , t ( ) } | { :error , any ( ) }
58123 def from_image ( % Image { id: image_id , path: path } = image ) do
59124 with { :read_file , { :ok , data } } <- { :read_file , File . read ( path ) } ,
@@ -79,13 +144,16 @@ defmodule Memorable.Data.Image.DerivedMetadata do
79144 end
80145 end
81146
147+ # Parses the `DateTimeOriginal` field in EXIF into a `NaiveDateTime`.
82148 @ spec original_datetime ( Image . metadata ( ) ) :: NaiveDateTime . t ( ) | nil
83149 defp original_datetime ( metadata ) do
84150 with result when result != nil <- Map . get ( metadata , "DateTimeOriginal" ) do
85151 NaiveDateTime . from_iso8601! ( result )
86152 end
87153 end
88154
155+ # Parses the `FocalLength` field in EXIF into a float, discarding the unit measurement.
156+ # Currently assumes `exiftool` always returns focal length in millimetres, and fails if it doesn't.
89157 @ spec focal_length ( Image . metadata ( ) ) :: float ( ) | nil
90158 defp focal_length ( metadata ) do
91159 with result when result != nil <- Map . get ( metadata , "FocalLength" ) do
0 commit comments