PhoenixKit.Modules.Storage.Exif (phoenix_kit v2.60.2)

Copy Markdown View Source

What a photo's EXIF says, in two forms: a summary worth keeping, and the whole dump to look at.

ImageMagick (CaptureDate.read_exif/1) prints every tag as text, most of them as fractions: FNumber=1433/512, GPSLatitude=45/1,28/1,1199/100, ExposureTime=10/1250. This module turns that into something a person or a map can use.

The summary

summary/1 is what is stored in the file's metadata["exif"]: a small, JSON-safe map with only the groups the photo has.

%{
  "camera"   => %{"make" => "Apple", "model" => "iPhone 17 Pro",
                  "lens_model" => "...", "software" => "27.0"},
  "exposure" => %{"focal_length" => 16.9, "focal_length_35mm" => 200,
                  "f_number" => 2.8, "exposure_time" => "1/125", "iso" => 50,
                  "flash" => false},
  "dates"    => %{"original" => "2026-10-05T18:42:46", "digitized" => "...",
                  "modified" => "...", "offset" => "+02:00"},
  "gps"      => %{"latitude" => 45.4698, "longitude" => 10.7209,
                  "altitude" => 105.0, "speed_kmh" => 0.9, "direction" => 352.8,
                  "timestamp" => "2026-10-05T16:42:46Z"},
  "orientation" => 1
}

A photo whose tags fall in none of these groups has only %{"tags" => count}, so it can be told from one with no EXIF at all (%{}).

The camera's serial number and the maker note are not kept. The position is also written to the file's latitude / longitude columns (coordinates/1), the copy a map searches.

The whole dump

groups/1 lists every tag the file carries, grouped as web EXIF viewers do (Camera, Exposure, Dates, Location, Image, Other), with fractions shown as numbers. It is read from the original on request, never stored.

Summary

Functions

The GPS position of tags as {latitude, longitude} in decimal degrees, or nil: no position, an impossible one, or the 0, 0 a camera without a fix writes.

What to record on a file for tags: %{exif: summary, latitude: lat, longitude: lon} (the position nil when there is none). summary is %{} for a photo with no EXIF: stored as such, it says "read, and there was nothing", which is not the same as never read.

The titles of the groups groups/1 uses, in order: [{group, title}].

Every tag in tags, grouped for a viewer: [{group, [{label, value}]}] in the order of group_titles/0, empty groups left out. Fractions are shown as numbers and a GPS position as decimal degrees.

The EXIF tags of the image at path (CaptureDate.read_exif/1): %{} when it has none.

The curated, JSON-safe summary of tags; %{} when there is nothing worth keeping.

Types

tags()

@type tags() :: %{required(String.t()) => String.t()}

Functions

coordinates(tags)

@spec coordinates(tags()) :: {float(), float()} | nil

The GPS position of tags as {latitude, longitude} in decimal degrees, or nil: no position, an impossible one, or the 0, 0 a camera without a fix writes.

file_attrs(tags)

@spec file_attrs(tags()) :: %{
  exif: map(),
  latitude: float() | nil,
  longitude: float() | nil
}

What to record on a file for tags: %{exif: summary, latitude: lat, longitude: lon} (the position nil when there is none). summary is %{} for a photo with no EXIF: stored as such, it says "read, and there was nothing", which is not the same as never read.

group_titles()

The titles of the groups groups/1 uses, in order: [{group, title}].

groups(tags)

@spec groups(tags()) :: [{atom(), [{String.t(), String.t()}]}]

Every tag in tags, grouped for a viewer: [{group, [{label, value}]}] in the order of group_titles/0, empty groups left out. Fractions are shown as numbers and a GPS position as decimal degrees.

read(path)

@spec read(Path.t()) :: tags()

The EXIF tags of the image at path (CaptureDate.read_exif/1): %{} when it has none.

summary(tags)

@spec summary(tags()) :: map()

The curated, JSON-safe summary of tags; %{} when there is nothing worth keeping.