Packages

Read TIFF and EXIF information from a JPEG-format image. iex> {:ok, info} = Exexif.exif_from_jpeg_buffer(buffer) iex> info.x_resolution 72 iex> info.model "DSC-RX100M2" ...> Exexif.Data.Gps.inspect info "41°23´16˝N,2°11´50˝E"

Current section

Files

Jump to
exexif lib exexif.ex
Raw

lib/exexif.ex

defmodule Exexif do
alias Exexif.Decode
alias Exexif.Tag
@max_exif_len 2*(65536+2)
@image_start_marker 0xffd8
# @image_end_marker 0xffd9 # NOT USED
@app1_marker 0xffe1
def exif_from_jpeg_file(name) when is_binary(name) do
{:ok, buffer} = File.open(name,
[:read],
fn (file) ->
IO.binread(file, @max_exif_len)
end)
exif_from_jpeg_buffer(buffer)
end
def exif_from_jpeg_file!(name) when is_binary(name) do
case exif_from_jpeg_file(name) do
{:ok, result} -> result
{:error, error} -> raise(Exexif.ReadError, type: error, file: name)
end
end
def exif_from_jpeg_buffer(<< @image_start_marker :: 16, rest :: binary>>) do
read_exif(rest)
end
def exif_from_jpeg_buffer(_), do: {:error, :not_a_jpeg_file}
def exif_from_jpeg_buffer!(buffer) do
case exif_from_jpeg_buffer(buffer) do
{:ok, result} -> result
{:error, error} -> raise Exexif.ReadError, type: error, file: nil
end
end
def read_exif(<<
@app1_marker :: 16,
_len :: 16,
"Exif" :: binary,
0 :: 16,
exif :: binary
>>) do
<<
byte_order :: 16,
forty_two :: binary-size(2),
offset :: binary-size(4),
_rest :: binary >> = exif
endian = case byte_order do
0x4949 -> :little
0x4d4d -> :big
end
read_unsigned = fn (value) -> :binary.decode_unsigned(value, endian) end
42 = read_unsigned.(forty_two) # sanity check
offset = read_unsigned.(offset)
{ :ok, reshape(read_ifd({exif, offset, read_unsigned})) }
end
def read_exif(<< 0xff :: 8, _number :: 8, len :: 16, data :: binary>>) do
# IO.puts "Skip segment #{number}"
skip_segment(len-2, data)
|> read_exif()
end
def read_exif(_) do
{ :error, :no_exif_data_in_jpeg }
end
def skip_segment(len, data) do
<< _segment :: size(len)-unit(8), rest :: binary >> = data
rest
end
def read_ifd({exif, offset, ru} = context) do
<< _ :: binary-size(offset), tag_count :: binary-size(2), tags :: binary >> = exif
read_tags(ru.(tag_count), tags, context, :tiff, [])
end
def read_tags(0, _tags, _context, _type, result) do
Enum.into(result, %{})
end
def read_tags(count,
<<
tag :: binary-size(2),
format :: binary-size(2),
component_count :: binary-size(4),
value :: binary-size(4),
rest :: binary
>>,
{_exif, _offset, ru} = context,
type,
result) do
tag = ru.(tag)
format = ru.(format)
component_count = ru.(component_count)
value = Tag.value(format, component_count, value, context)
{name, description} = Decode.tag(type, tag, value)
kv = case name do
:exif -> { :exif, read_exif(value, context) }
:gps -> { :gps, read_gps(value, context) }
_ -> { name, description }
end
read_tags(count-1, rest, context, type, [ kv | result ])
end
def read_tags(_, _, _, _, result) do # Handle malformed data
Enum.into(result, %{})
end
def read_exif(exif_offset, {exif, _offset, ru} = context) do
<< _ :: binary-size(exif_offset), count :: binary-size(2), tags :: binary >> = exif
count = ru.(count)
read_tags(count, tags, context, :exif, [])
end
def read_gps(gps_offset, {gps, _offset, ru} = context) do
<< _ :: binary-size(gps_offset), count :: binary-size(2), tags :: binary >> = gps
struct(Exexif.Data.Gps, read_tags(ru.(count), tags, context, :gps, []))
end
defp reshape(result) do
result
|> extract_thumbnail
end
defp extract_thumbnail(result) do
exif_keys = Map.keys(result.exif)
result = if Enum.all?(Exexif.Data.Thumbnail.fields, fn e -> Enum.any?(exif_keys, & &1 == e) end) do
Map.put(
result,
:thumbnail,
struct(
Exexif.Data.Thumbnail,
Exexif.Data.Thumbnail.fields
|> Enum.map(fn e -> {e, result.exif[e]} end)
|> Enum.into(%{})
)
)
else
result
end
%{result | exif: Map.drop(result.exif, Exexif.Data.Thumbnail.fields)}
end
end