Current section

Files

Jump to
stb_image lib stb_image.ex
Raw

lib/stb_image.ex

defmodule StbImage do
@moduledoc """
Tiny image encoding and decoding.
The following formats are supported:
* JPEG baseline & progressive (12 bpc/arithmetic not supported, same as stock IJG lib)
* PNG 1/2/4/8/16-bit-per-channel
* TGA
* BMP non-1bpp, non-RLE
* PSD (composited view only, no extra channels, 8/16 bit-per-channel)
* GIF (always reports as 4-channel)
* HDR (radiance rgbE format)
* PIC (Softimage PIC)
* PNM (PPM and PGM binary only)
There are also specific functions for working with GIFs.
"""
defguardp is_path(path) when is_binary(path) or is_list(path)
@doc """
The `StbImage` struct.
It has the following fields:
* `:data` - a blob with the image bytes in HWC (heigth-width-channels) order
* `:shape` - a tuple with the `{height, width, channels}`
* `:type` - the type unit in the binary (u8/u16/f32)
* `:color_mode` - the color mode as `:l`, `:la`, `:rgb`, or `:rgba`
"""
defstruct [:data, :shape, :type, :color_mode]
@doc """
Creates a StbImage directly.
`data` is a binary blob with the image bytes in HWC
(heigth-width-channels) order. `shape` is a tuple
with the `heigth`, `width`, and `channel` dimensions.
## Options
* `:color_mode` - the color mode. One is automatically
inferred from the number of channels.
* `:type` - The type of the data. Defaults to `:u8`.
Must be one of `:u8`, `:u16`, `:f32`.
"""
def new(data, {h, w, c} = shape, opts \\ []) when is_binary(data) and is_integer(h) and h > 0 and is_integer(w) and w > 0 and c in 1..4 do
color_mode = opts[:color_mode] || elem({:l, :la, :rgb, :rgba}, c - 1)
type = opts[:type] || :u8
if byte_size(data) == h * w * c * bytes(type) do
%StbImage{data: data, shape: shape, color_mode: color_mode, type: type}
else
raise ArgumentError, "cannot create StbImage because number of bytes do not match shape and type"
end
end
defp bytes(:u8), do: 1
defp bytes(:u16), do: 2
defp bytes(:f32), do: 4
@compile {:no_warn_undefined, Nx}
@compile {:no_warn_undefined, Nx.Type}
@doc """
Converts a `StbImage` to a Nx tensor.
It accepts the same options as `Nx.from_binary/3`.
"""
def to_nx(%StbImage{data: data, type: type, shape: shape}, opts \\ []) do
data
|> Nx.from_binary(Nx.Type.normalize!(type), opts)
|> Nx.reshape(shape)
end
@doc """
Creates a `StbImage` from a Nx tensor.
The tensor is expected to have shape `{h, w, c}`
and one of the supported types.
"""
def from_nx(tensor) when is_struct(tensor, Nx.Tensor) do
new(Nx.to_binary(tensor), tensor_shape(Nx.shape(tensor)), type: tensor_type(Nx.type(tensor)))
end
defp tensor_type({:u, 8}), do: :u8
defp tensor_type({:u, 16}), do: :u16
defp tensor_type({:f, 32}), do: :f32
defp tensor_type(type), do: raise ArgumentError, "unsupported tensor type: #{inspect(type)}"
defp tensor_shape({_, _, _} = shape), do: shape
defp tensor_shape(shape), do: raise ArgumentError, "unsupported tensor shape: #{inspect(shape)}"
@doc """
Decodes image from file at `path`.
## Options
* `:channels` - The number of desired channels.
Use `0` for auto-detection. Defaults to 0.
* `:type` - The type of the data. Defaults to `:u8`.
Must be one of `:u8`, `:u16`, `:f32`.
## Example
{:ok, img} = StbImage.from_file("/path/to/image")
{h, w, c} = img.shape
data = img.data
# If you know the image is a 4-channel image and auto-detection failed
{:ok, img} = StbImage.from_file("/path/to/image", channels: 4)
{h, w, c} = img.shape
img = img.data
"""
def from_file(path, opts \\ []) when is_path(path) and is_list(opts) do
type = opts[:type] || :u8
channels = opts[:channels] || 0
with {:ok, img, shape, type, channels} <-
StbImage.Nif.from_file(path_to_charlist(path), channels, type) do
{:ok, %StbImage{data: img, shape: shape, type: type, color_mode: channels}}
end
end
@doc """
Decodes image from `binary` representing an image.
## Options
* `:channels` - The number of desired channels.
Use `0` for auto-detection. Defaults to 0.
* `:type` - The type of the data. Defaults to `:u8`.
Must be one of `:u8`, `:u16`, `:f32`.
## Example
{:ok, buffer} = File.read("/path/to/image")
{:ok, img} = StbImage.from_binary(buffer)
{h, w, c} = img.shape
img = img.data
# If you know the image is a 4-channel image and auto-detection failed
{:ok, img} = StbImage.from_file("/path/to/image", channels: 4)
{h, w, c} = img.shape
img = img.data
"""
def from_binary(buffer, opts \\ []) when is_binary(buffer) and is_list(opts) do
type = opts[:type] || :u8
channels = opts[:channels] || 0
with {:ok, img, shape, type, channels} <- StbImage.Nif.from_binary(buffer, channels, type) do
{:ok, %StbImage{data: img, shape: shape, type: type, color_mode: channels}}
end
end
@doc """
Decodes GIF image from file at `path`.
## Example
{:ok, frames, delays} = StbImage.gif_from_file("/path/to/image")
frame = Enum.at(frames, 0)
{h, w, 3} = frame.shape
# GIFs always have channels == :rgb and type == :u8
# delays is a list that has n elements, where n is the number of frames
"""
def gif_from_file(path) when is_binary(path) or is_list(path) do
with {:ok, binary} <- File.read(path) do
gif_from_binary(binary)
end
end
@doc """
Decodes GIF image from a `binary` representing a GIF.
## Example
{:ok, buffer} = File.read("/path/to/image")
{:ok, frames, delays} = StbImage.gif_from_binary(buffer)
frame = Enum.at(frames, 0)
{h, w, 3} = frame.shape
# GIFs always have channels == :rgb and type == :u8
# delays is a list that has n elements, where n is the number of frames
"""
def gif_from_binary(binary) when is_binary(binary) do
with {:ok, frames, shape, delays} <- StbImage.Nif.gif_from_binary(binary) do
stb_frames =
for frame <- frames, do: %StbImage{data: frame, shape: shape, type: :u8, color_mode: :rgb}
{:ok, stb_frames, delays}
end
end
@encoding_formats ~w(jpg png bmp tga)a
@encoding_formats_string Enum.map_join(@encoding_formats, ", ", &inspect/1)
@doc """
Saves image to the file at `path`.
The supported formats are #{@encoding_formats_string}.
The format is determined from the file extension if possible,
you can also pass it explicitly via the `:format` option.
Returns `:ok` on success and `{:error, reason}` otherwise.
Make sure the directory you intent to write the file to exists,
otherwise an error is returned.
## Options
* `:format` - one of the supported image formats
"""
def to_file(%StbImage{data: data, shape: {height, width, channels}}, path, opts \\ []) do
format = opts[:format] || format_from_path!(path)
assert_encoding_format!(format)
StbImage.Nif.to_file(path_to_charlist(path), format, data, height, width, channels)
end
@doc """
Encodes image to a binary.
The supported formats are #{@encoding_formats_string}.
## Example
img = %StbImage{data: raw_img, shape: {h, w, channels}, type: :u8, color_mode: :rgba}
{:ok, binary} = StbImage.to_binary(img, :png)
"""
def to_binary(%StbImage{data: data, shape: {height, width, channels}}, format) do
assert_encoding_format!(format)
StbImage.Nif.to_binary(format, data, height, width, channels)
end
defp format_from_path!(path) do
case Path.extname(path) do
".jpg" ->
:jpg
".jpeg" ->
:jpg
".png" ->
:png
".bmp" ->
:bmp
".tga" ->
:tga
ext ->
raise "could not determine a supported encoding format for file #{inspect(path)} with extension #{inspect(ext)}, " <>
"please specify a supported :format option explicitly"
end
end
defp assert_encoding_format!(format) do
unless format in @encoding_formats do
raise ArgumentError,
"got an unsupported encoding format #{inspect(format)}, " <>
"the format must be one of #{inspect(@encoding_formats)}"
end
end
defp path_to_charlist(path) when is_list(path), do: path
defp path_to_charlist(path) when is_binary(path), do: String.to_charlist(path)
end