Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for previewing image in Livebook with Kino #25

Merged
merged 12 commits into from
Nov 8, 2022
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ endif
.DEFAULT_GLOBAL := build

build: $(STB_IMAGE_NIF_SO)
@ echo > /dev/null

$(STB_IMAGE_NIF_SO):
@ mkdir -p $(PRIV_DIR)
Expand Down
202 changes: 202 additions & 0 deletions lib/stb_image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ defmodule StbImage do
There are also specific functions for working with GIFs.
"""

require Logger

@doc """
The `StbImage` struct.

Expand Down Expand Up @@ -103,6 +105,206 @@ defmodule StbImage do
"unsupported tensor shape: #{inspect(shape)} (expected height-width-channel)"
)

@compile {:no_warn_undefined, Kino}

cocoa-xu marked this conversation as resolved.
Show resolved Hide resolved
if Code.ensure_loaded?(Kino.Render) do
defimpl Kino.Render do
require Logger

@default_kino_render_encoding Application.compile_env(
:stb_image,
:kino_render_encoding,
:png
)
@doc """
Get preferred image encoding when rendering in Kino.

Default value is `Application.compile_env(:stb_image, :kino_render_encoding, :png)`.
"""
@spec kino_render_encoding(atom()) :: term()
def kino_render_encoding(encoding \\ nil)

def kino_render_encoding(nil) do
Process.get(:stb_image_kino_render_encoding, @default_kino_render_encoding)
end

def kino_render_encoding(encoding) when encoding in [:png, :jpg, :jpeg] do
encoding =
if encoding == :jpeg do
:jpg
else
encoding
end

Process.put(:stb_image_kino_render_encoding, encoding)
end

def kino_render_encoding(unsupported_encoding) do
raise RuntimeError, "Encoding #{inspect(unsupported_encoding)} is not supported."
end
cocoa-xu marked this conversation as resolved.
Show resolved Hide resolved

@default_kino_render_max_size Application.compile_env(
:stb_image,
:kino_render_max_size,
{8192, 8192}
)
@doc """
Get the maximum allowed image size to render in Kino.

Default value is `Application.compile_env(:stb_image, :kino_render_max_size, {8192, 8192})`.
"""
@spec kino_render_max_size({integer(), integer()} | nil) :: term()
def kino_render_max_size(max_size \\ nil)

def kino_render_max_size(nil) do
Process.get(:stb_image_kino_render_max_size, @default_kino_render_max_size)
end

def kino_render_max_size({height, width}) when is_integer(height) and is_integer(width) do
Process.put(:stb_image_kino_render_max_size, {height, width})
end

def kino_render_max_size(unsupported_max_size) do
raise RuntimeError, """
Invalid parameter for max size, expecting a 2-tuple, {height, width},
where height and width are both integers. However, got `#{inspect(unsupported_max_size)}`
"""
end

@supported_kino_render_tab_order (if Code.ensure_loaded?(Nx) do
[:image, :raw, :numerical]
else
[:image, :raw]
end)
@kino_render_tab_order Enum.uniq(
Application.compile_env(
:stb_image,
:kino_render_tab_order,
@supported_kino_render_tab_order
)
)
@doc """
Get preferred order of Kino.Layout tabs for `StbImage` in Livebook.

Default value is `Enum.uniq(Application.compile_env(:stb_image, :kino_render_tab_order, @supported_kino_render_tab_order))`.

where `@supported_kino_render_tab_order` is `[:image, :raw, :numerical]` if `:nx` is available,
otherwise `[:image, :raw]`
"""
@spec kino_render_tab_order([atom()] | nil) :: term()
def kino_render_tab_order(order \\ nil)

def kino_render_tab_order(nil) do
Process.get(:stb_image_kino_render_tab_order, @kino_render_tab_order)
end

def kino_render_tab_order(order) when is_list(order) do
render_types =
Enum.map(order, fn t ->
supported? = Enum.member?(@supported_kino_render_tab_order, t)

if !supported? do
Logger.warning("""
Unknown type `#{inspect(t)}` found in `config :stb_image, kino_render_tab_order`.
Supported types are `#{inspect(@supported_kino_render_tab_order)}` and their combinations.
""")

nil
else
t
end
end)
|> Enum.reject(fn a -> a == nil end)

Process.put(:stb_image_kino_render_tab_order, render_types)
end

def kino_render_tab_order(types) do
raise RuntimeError, """
Unknown order `#{inspect(types)}`.
Supported orders are `#{inspect(@supported_kino_render_tab_order)}` and their combinations.
"""
end

defp within_maximum_size(image) do
{max_height, max_width} = kino_render_max_size()

case image.shape do
{h, w} ->
h <= max_height and w <= max_width

{h, w, _c} ->
h <= max_height and w <= max_width

_ ->
false
end
end

@spec to_livebook(StbImage.t()) :: Kino.Output.t()
def to_livebook(image) when is_struct(image, StbImage) do
render_types = kino_render_tab_order()

Enum.map(render_types, fn
:raw ->
{"Raw", Kino.Inspect.new(image)}

:numerical ->
if Code.ensure_loaded?(Nx) do
{"Numerical", Kino.Inspect.new(StbImage.to_nx(image))}
else
{"Numerical",
Kino.Markdown.new("""
The `Numerical` tab requires application `:nx`, please add `{:nx, "~> 0.4"}` to the dependency list.
""")}
end

:image ->
{stb_format, kino_format} =
case kino_render_encoding() do
:jpg ->
{:jpg, :jpeg}

:jpeg ->
{:jpg, :jpeg}

:png ->
{:png, :png}

unknown ->
raise RuntimeError, "Cannot render image with encoding `#{inspect(unknown)}`"
end

with true <- within_maximum_size(image),
encoded <- StbImage.to_binary(image, stb_format),
true <- is_binary(encoded) do
{"Image", Kino.Image.new(encoded, kino_format)}
else
_ ->
nil
end
end)
|> Enum.reject(fn a -> a == nil end)
josevalim marked this conversation as resolved.
Show resolved Hide resolved
|> to_livebook_tabs(render_types, image)
end

defp to_livebook_tabs([], [:image], image) do
Kino.Layout.tabs([{"Raw", Kino.Inspect.new(image)}])
|> Kino.Render.to_livebook()
end

defp to_livebook_tabs(_tabs, [], image) do
Kino.Inspect.new(image)
|> Kino.Render.to_livebook()
end

defp to_livebook_tabs(tabs, _types, _mat) do
Kino.Layout.tabs(tabs)
|> Kino.Render.to_livebook()
end
end
end

@doc """
Reads image from file at `path`.

Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ defmodule StbImage.MixProject do
[
{:elixir_make, "~> 0.6"},
{:nx, "~> 0.1", optional: true},
{:kino, "~> 0.7", optional: true},
{:ex_doc, "~> 0.23", only: :docs, runtime: false}
]
end
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"},
"kino": {:hex, :kino, "0.7.0", "98d2623bebf8d1c4d761c5ed9741c6aae2f1909706baec501f2fdad347cb9d92", [:mix], [{:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "3dd1ee761c241caa30364333e3102a954d68188414a46e867f95c858d745f3fd"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"nx": {:hex, :nx, "0.2.0", "ae550cd72bca2e56f90fc63d59f2f11ac17ed3c4a9cd0968397f4ef9b795b3d6", [:mix], [{:complex, "~> 0.4.0", [hex: :complex, repo: "hexpm", optional: false]}], "hexpm", "31d4936d2cf78a0b147830f0e6fca45bd3ed4b1e592e2bcd61a85f81e892617c"},
"table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
}