Skip to content

Commit

Permalink
add support for the AVIF, HEIC and HEIF formats
Browse files Browse the repository at this point in the history
These 3 formats have in common that they use the HEIF container format specified in ISO/IEC 23008-12.

The MIME types and variant types (file extension) for HEIC and HEIF are based on sections C.2 and D.2 of the specification.

A HEIF container can contain multiple images, so the logic is kept as in `:ico` (take the size of the largest image).

The size of an image is found in the `ispe` box.
  • Loading branch information
bu6n committed Sep 9, 2024
1 parent 8588595 commit d7b772b
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 5 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Supported formats (image type to be parsed as):
- `:psd`
- `:tiff`
- `:webp` (VP8X animated in `v0.2.4`)
- `:avif`
- `:heic`
- `:heif`

## Mime-types and Variants

Expand All @@ -68,9 +71,14 @@ Each mime-type can be linked to at least one variant type:

| mime-type | variant type | description |
| ------------------------- | ------------ | ------------------ |
| `image/avif` | `AVIF` | |
| `image/bmp` | `BMP` | |
| `image/gif` | `GIF87a` | 87a gif spec |
| `image/gif` | `GIF89a` | 89a gif spec |
| `image/heic` | `HEIC` | |
| `image/heic-sequence` | `HEICS` | |
| `image/heif` | `HEIF` | |
| `image/heif-sequence` | `HEIFS` | |
| `image/x-icon` | `ICO` | |
| `image/jpeg` | `baseJPEG` | baseline JPEG |
| `image/jpeg` | `progJPEG` | progressive JPEG |
Expand All @@ -89,10 +97,10 @@ Each mime-type can be linked to at least one variant type:
The variant type is created just to provide a bit more of information
for every image format (if applicable).

*Note*: `:ico` returns the dimensions of the largest image contained (not the first found).
*Note*: `:avif`, `:heic`, `:heif` and `:ico` return the dimensions of the largest image contained (not the first found).

The guessing functions try to detect the format of the binary by testing every available type based on its global usage (popularity, [usage of image file formats](https://w3techs.com/technologies/overview/image_format/all), but still keeping the `:png` as the first one):
- `:png`, `:jpeg`, `:gif`, `:bmp`, `:ico`, `:tiff`, `:webp`, `:psd`, `:jp2`, `:pnm`
- `:png`, `:jpeg`, `:gif`, `:bmp`, `:ico`, `:tiff`, `:webp`, `:psd`, `:jp2`, `:pnm`, `:avif`, `:heic`, `:heif`

**Warnings:**

Expand Down
51 changes: 48 additions & 3 deletions lib/ex_image_info.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
defmodule ExImageInfo do
alias ExImageInfo.Types.{BMP, GIF, ICO, JP2, JPEG, PNG, PNM, PSD, TIFF, WEBP}
alias ExImageInfo.Types.{
BMP,
GIF,
ICO,
JP2,
JPEG,
PNG,
PNM,
PSD,
TIFF,
WEBP,
AVIF,
HEIC,
HEIF
}

@moduledoc """
ExImageInfo is an Elixir library to parse images (binaries) and get the dimensions (size), detected mime-type and overall validity for a set of image formats. Main module to parse a binary and get if it seems to be an image (validity), the mime-type (and variant detected) and the dimensions of the image, based on a specific image format.
Expand Down Expand Up @@ -43,9 +57,14 @@ defmodule ExImageInfo do
| mime-type | variant type | description |
| ------------------------- | ------------ | ------------------ |
| `image/avif` | `AVIF` | |
| `image/bmp` | `BMP` | |
| `image/gif` | `GIF87a` | 87a gif spec |
| `image/gif` | `GIF89a` | 89a gif spec |
| `image/heic` | `HEIC` | |
| `image/heic-sequence` | `HEICS` | |
| `image/heif` | `HEIF` | |
| `image/heif-sequence` | `HEIFS` | |
| `image/x-icon` | `ICO` | |
| `image/jpeg` | `baseJPEG` | baseline JPEG |
| `image/jpeg` | `progJPEG` | progressive JPEG |
Expand All @@ -67,13 +86,27 @@ defmodule ExImageInfo do
*Note*: `:ico` returns the dimensions of the largest image contained (not the first found).
The guessing functions try to detect the format of the binary by testing every available type based on its global usage (popularity, [usage of image file formats](https://w3techs.com/technologies/overview/image_format/all), but still keeping the `:png` as the first one):
- `:png`, `:jpeg`, `:gif`, `:bmp`, `:ico`, `:tiff`, `:webp`, `:psd`, `:jp2`, `:pnm`
- `:png`, `:jpeg`, `:gif`, `:bmp`, `:ico`, `:tiff`, `:webp`, `:psd`, `:jp2`, `:pnm`, `:avif`, `:heic`, `:heif`
"""

# Guessing function ordered by global usage
# https://w3techs.com/technologies/overview/image_format/all
# but still keeping :png as the first
@types [:png, :jpeg, :gif, :bmp, :ico, :tiff, :webp, :psd, :jp2, :pnm]
@types [
:png,
:jpeg,
:gif,
:bmp,
:ico,
:tiff,
:webp,
:psd,
:jp2,
:pnm,
:avif,
:heic,
:heif
]

@typedoc "The supported image formats"
@type image_format ::
Expand All @@ -87,6 +120,9 @@ defmodule ExImageInfo do
| :psd
| :jp2
| :pnm
| :avif
| :heic
| :heif

## Public API

Expand Down Expand Up @@ -130,6 +166,9 @@ defmodule ExImageInfo do
def seems?(binary, :jp2), do: JP2.seems?(binary)
def seems?(binary, :pnm), do: PNM.seems?(binary)
def seems?(binary, :ico), do: ICO.seems?(binary)
def seems?(binary, :avif), do: AVIF.seems?(binary)
def seems?(binary, :heic), do: HEIC.seems?(binary)
def seems?(binary, :heif), do: HEIF.seems?(binary)
def seems?(_, _), do: nil

@doc """
Expand Down Expand Up @@ -201,6 +240,9 @@ defmodule ExImageInfo do
def type(binary, :jp2), do: JP2.type(binary)
def type(binary, :pnm), do: PNM.type(binary)
def type(binary, :ico), do: ICO.type(binary)
def type(binary, :avif), do: AVIF.type(binary)
def type(binary, :heic), do: HEIC.type(binary)
def type(binary, :heif), do: HEIF.type(binary)
def type(_, _), do: nil

@doc """
Expand Down Expand Up @@ -272,6 +314,9 @@ defmodule ExImageInfo do
def info(binary, :jp2), do: JP2.info(binary)
def info(binary, :pnm), do: PNM.info(binary)
def info(binary, :ico), do: ICO.info(binary)
def info(binary, :avif), do: AVIF.info(binary)
def info(binary, :heic), do: HEIC.info(binary)
def info(binary, :heif), do: HEIF.info(binary)
def info(_, _), do: nil

@doc """
Expand Down
22 changes: 22 additions & 0 deletions lib/ex_image_info/types/avif.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule ExImageInfo.Types.AVIF do
@moduledoc false

@behaviour ExImageInfo.Detector

@mime "image/avif"
@signature <<"ftypavif">>
@ftype "AVIF"

## Public API

def seems?(<<_::size(32), @signature, _rest::binary>>), do: true
def seems?(_), do: false

def info(<<_::size(32), @signature, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime, @ftype)

def info(_), do: nil

def type(<<_::size(32), @signature, _rest::binary>>), do: {@mime, @ftype}
def type(_), do: nil
end
78 changes: 78 additions & 0 deletions lib/ex_image_info/types/heic.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
defmodule ExImageInfo.Types.HEIC do
@moduledoc false

@behaviour ExImageInfo.Detector

@mime_heic "image/heic"
@mime_heic_sequence "image/heic-sequence"
@signature_heic <<"ftypheic">>
@signature_heim <<"ftypheim">>
@signature_heis <<"ftypheis">>
@signature_heix <<"ftypheix">>
@signature_hevc <<"ftyphevc">>
@signature_hevm <<"ftyphevm">>
@signature_hevs <<"ftyphevs">>
@signature_hevx <<"ftyphevx">>
@ftype_heic "HEIC"
@ftype_heic_sequence "HEICS"

## Public API

def seems?(<<_::size(32), @signature_heic, _rest::binary>>), do: true
def seems?(<<_::size(32), @signature_heim, _rest::binary>>), do: true
def seems?(<<_::size(32), @signature_heis, _rest::binary>>), do: true
def seems?(<<_::size(32), @signature_heix, _rest::binary>>), do: true
def seems?(_), do: false

def info(<<_::size(32), @signature_heic, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic, @ftype_heic)

def info(<<_::size(32), @signature_heim, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic, @ftype_heic)

def info(<<_::size(32), @signature_heis, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic, @ftype_heic)

def info(<<_::size(32), @signature_heix, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic, @ftype_heic)

def info(<<_::size(32), @signature_hevc, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic_sequence, @ftype_heic_sequence)

def info(<<_::size(32), @signature_hevm, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic_sequence, @ftype_heic_sequence)

def info(<<_::size(32), @signature_hevs, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic_sequence, @ftype_heic_sequence)

def info(<<_::size(32), @signature_hevx, rest::binary>>),
do: ExImageInfo.Types.HEIF.info(rest, @mime_heic_sequence, @ftype_heic_sequence)

def info(_), do: nil

def type(<<_::size(32), @signature_heic, _rest::binary>>),
do: {@mime_heic, @ftype_heic}

def type(<<_::size(32), @signature_heim, _rest::binary>>),
do: {@mime_heic, @ftype_heic}

def type(<<_::size(32), @signature_heis, _rest::binary>>),
do: {@mime_heic, @ftype_heic}

def type(<<_::size(32), @signature_heix, _rest::binary>>),
do: {@mime_heic, @ftype_heic}

def type(<<_::size(32), @signature_hevc, _rest::binary>>),
do: {@mime_heic_sequence, @ftype_heic_sequence}

def type(<<_::size(32), @signature_hevm, _rest::binary>>),
do: {@mime_heic_sequence, @ftype_heic_sequence}

def type(<<_::size(32), @signature_hevs, _rest::binary>>),
do: {@mime_heic_sequence, @ftype_heic_sequence}

def type(<<_::size(32), @signature_hevx, _rest::binary>>),
do: {@mime_heic_sequence, @ftype_heic_sequence}

def type(_), do: nil
end
59 changes: 59 additions & 0 deletions lib/ex_image_info/types/heif.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule ExImageInfo.Types.HEIF do
@moduledoc false

@behaviour ExImageInfo.Detector

@mime_heif "image/heif"
@mime_heif_sequence "image/heif-sequence"
@signature_mif1 <<"ftypmif1">>
@signature_msf1 <<"ftypmsf1">>
@ftype_heif "HEIF"
@ftype_heif_sequence "HEIFS"

## Public API

def seems?(<<_::size(32), @signature_mif1, _rest::binary>>), do: true
def seems?(<<_::size(32), @signature_msf1, _rest::binary>>), do: true
def seems?(_), do: false

def info(<<_::size(32), @signature_mif1, rest::binary>>) do
info(rest, @mime_heif, @ftype_heif)
end

def info(<<_::size(32), @signature_msf1, rest::binary>>),
do: info(rest, @mime_heif_sequence, @ftype_heif_sequence)

def info(_), do: nil

def info(binary, mime, ftype) do
case size(binary) do
{w, h} -> {mime, w, h, ftype}
_ -> nil
end
end

def type(<<_::size(32), @signature_mif1, _rest::binary>>),
do: {@mime_heif, @ftype_heif}

def type(<<_::size(32), @signature_msf1, _rest::binary>>),
do: {@mime_heif_sequence, @ftype_heif_sequence}

def type(_), do: nil

defp size(binary) do
[_hd | ispe_boxes] = String.split(binary, "ispe")

if length(ispe_boxes) > 0 do
{width, height} =
ispe_boxes
|> Enum.map(fn <<_::size(32), width::size(32), height::size(32), _rest::binary>> ->
{width, height}
end)
|> Enum.max_by(&elem(&1, 0))

{width, height}
else
nil
end
end
end

0 comments on commit d7b772b

Please sign in to comment.