Skip to content

Commit 24f1e8b

Browse files
committed
image: initial derived metadata impl
1 parent 7de171b commit 24f1e8b

File tree

4 files changed

+110
-4
lines changed

4 files changed

+110
-4
lines changed

lib/data/collection.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ defmodule Memorable.Data.Collection do
1515
@derive {Inspect, only: [:id, :name, :created_datetime]}
1616
use Memento.Table, attributes: [:id, :name, :created_datetime]
1717

18+
@type id :: Memorable.Util.id()
1819
@type t :: %__MODULE__{
19-
id: Memorable.Util.id(),
20+
id: __MODULE__.id(),
2021
name: String.t(),
2122
created_datetime: DateTime.t()
2223
}

lib/data/image.ex

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,96 @@
11
defmodule Memorable.Data.Image do
2-
use Memento.Table, attributes: [:id, :collection_id, :path, :timestamp, :upload_date]
2+
@derive {Inspect, except: []}
3+
use Memento.Table, attributes: [:id, :collection_id, :path, :imported_datetime]
4+
5+
@type t :: %__MODULE__{
6+
id: Memorable.Util.id(),
7+
collection_id: Memorable.Data.Collection.id(),
8+
path: Path.t(),
9+
imported_datetime: DateTime.t()
10+
}
11+
@type metadata :: %{String.t() => any()}
12+
13+
@spec read_metadata(t()) :: {:ok, metadata()} | {:error, any()}
14+
def read_metadata(%__MODULE__{path: path}) do
15+
with {:file_read, {:ok, data}} <- {:file_read, File.read(path)},
16+
%{status: 0, stdout: stdout} <- Subprocess.exiftool_json(data),
17+
{:json_decode, {:ok, result}} <- {:json_decode, JSON.decode(to_string(stdout))} do
18+
{:ok, List.first(result)}
19+
else
20+
{:file_read, {:error, reason}} -> {:error, {:file_read, reason}}
21+
%{status: nil} -> {:error, :exiftool_exit_signal}
22+
%{status: other} -> {:error, {:exiftool_exit_status, other}}
23+
{:json_decode, {:error, reason}} -> {:error, {:json_decode, reason}}
24+
end
25+
end
26+
end
27+
28+
defmodule Memorable.Data.Image.DerivedMetadata do
29+
alias Memorable.Data.Image
30+
31+
# `image_id` is 1:1 with an Image `id`
32+
use Memento.Table,
33+
attributes: [
34+
:image_id,
35+
:file_hash,
36+
:original_datetime,
37+
:lens_model,
38+
:body_model,
39+
:focal_length,
40+
:aperture,
41+
:exposure_time,
42+
:iso
43+
]
44+
45+
@type t :: %__MODULE__{
46+
image_id: Image.id(),
47+
file_hash: binary(),
48+
original_datetime: NaiveDateTime.t(),
49+
lens_model: String.t(),
50+
body_model: String.t(),
51+
focal_length: float(),
52+
aperture: float(),
53+
exposure_time: String.t(),
54+
iso: integer()
55+
}
56+
57+
@spec from_image(Image.t()) :: {:ok, t()} | {:error, any()}
58+
def from_image(%Image{id: image_id, path: path} = image) do
59+
with {:read_file, {:ok, data}} <- {:read_file, File.read(path)},
60+
{:read_metadata, {:ok, metadata}} <-
61+
{:read_metadata, Image.read_metadata(image)} do
62+
%__MODULE__{
63+
image_id: image_id,
64+
file_hash: :crypto.hash(:sha256, data),
65+
original_datetime: original_datetime(metadata),
66+
lens_model: Map.get(metadata, "LensID"),
67+
body_model: Map.get(metadata, "Model"),
68+
focal_length: focal_length(metadata),
69+
aperture: Map.get(metadata, "Aperture"),
70+
exposure_time: Map.get(metadata, "ExposureTime"),
71+
iso: Map.get(metadata, "ISO")
72+
}
73+
else
74+
{:read_file, error} -> {:error, {:read_file, error}}
75+
{:read_metadata, error} -> {:error, {:read_metadata, error}}
76+
end
77+
end
78+
79+
@spec original_datetime(Image.metadata()) :: NaiveDateTime.t() | nil
80+
defp original_datetime(metadata) do
81+
with result when result != nil <- Map.get(metadata, "DateTimeOriginal") do
82+
NaiveDateTime.from_iso8601!(result)
83+
end
84+
end
85+
86+
@spec focal_length(Image.metadata()) :: float() | nil
87+
defp focal_length(metadata) do
88+
with result when result != nil <- Map.get(metadata, "FocalLength") do
89+
case Float.parse(result) do
90+
{result, " mm"} -> result
91+
{_, _} -> raise "FocalLength is not in mm: #{result}"
92+
:error -> raise "FocalLength does not parse as float: #{result}"
93+
end
94+
end
95+
end
396
end

lib/memorable.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,36 @@ defmodule Memorable do
2525
Supervisor.start_link(
2626
[
2727
{Plug.Cowboy, plug: Memorable.Router, scheme: :http, options: [port: 4000]},
28-
{Task, &memento_test/0}
28+
{Task, &test/0}
2929
],
3030
strategy: :one_for_one
3131
)
3232
|> tap(fn _ -> Logger.info("memorable listening on port 4000") end)
3333
end
3434

35-
def memento_test() do
35+
def test() do
36+
alias Data.Image
37+
3638
Data.Collection.new("Test Collection")
3739
|> Data.Collection.rename("Renamed Collection")
3840
|> Data.Collection.write()
3941
|> IO.inspect()
4042

43+
image = %Image{
44+
id: 1,
45+
collection_id: nil,
46+
path: "test/data/20250317_0_0028_01.jpg",
47+
imported_datetime: DateTime.utc_now()
48+
}
49+
4150
# `Command` in rust needs to call waitpid(2), which fails with ECHILD when the signal handler
4251
# for SIGCHLD is set to SIG_IGN, as is done in the erlang vm.
4352
# <https://github.com/rusterlium/rustler/issues/446>
4453
# <http://erlang.org/pipermail/erlang-questions/2020-November/100109.html>
4554
:os.set_signal(:sigchld, :default)
55+
56+
Image.read_metadata(image) |> IO.inspect()
57+
Data.Image.DerivedMetadata.from_image(image) |> IO.inspect()
4658
end
4759
end
4860

test/data/20250317_0_0028_01.jpg

133 KB
Loading

0 commit comments

Comments
 (0)