diff --git a/Makefile b/Makefile index 7b7c4c9..ccc809e 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ endif .DEFAULT_GLOBAL := build build: $(STB_IMAGE_NIF_SO) + @ echo > /dev/null $(STB_IMAGE_NIF_SO): @ mkdir -p $(PRIV_DIR) diff --git a/lib/stb_image.ex b/lib/stb_image.ex index b33c9a4..9900aae 100644 --- a/lib/stb_image.ex +++ b/lib/stb_image.ex @@ -103,6 +103,96 @@ defmodule StbImage do "unsupported tensor shape: #{inspect(shape)} (expected height-width-channel)" ) + if Code.ensure_loaded?(Kino.Render) do + defimpl Kino.Render do + defp within_maximum_size(image) do + max_size = Application.fetch_env!(:stb_image, :kino_render_max_size) + + case max_size do + {max_height, max_width} when is_integer(max_height) and is_integer(max_width) -> + {h, w, _c} = image.shape + h <= max_height and w <= max_width + + _ -> + raise """ + invalid :kino_render_max_size configuration. Expected a 2-tuple, {height, width}, + where height and width are both integers. Got: #{inspect(max_size)} + """ + end + end + + @spec to_livebook(StbImage.t()) :: Kino.Output.t() + def to_livebook(image) when is_struct(image, StbImage) do + render_types = Application.fetch_env!(:stb_image, :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 -> + render_encoding = Application.fetch_env!(:stb_image, :kino_render_encoding) + + {stb_format, kino_format} = + case render_encoding do + :jpg -> + {:jpg, :jpeg} + + :jpeg -> + {:jpg, :jpeg} + + :png -> + {:png, :png} + + _ -> + raise "invalid :kino_render_encoding configuration. Expected one of :png, :jpg, or :jpeg. Got: #{inspect(render_encoding)}" + 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 + + type -> + raise """ + invalid :kino_render_tab_order configuration. The set of supported types are [:image, :raw, :numerical]. + Got: #{inspect(type)} + """ + end) + |> Enum.reject(&is_nil/1) + |> 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`. diff --git a/mix.exs b/mix.exs index 0c3fe52..7a9ec95 100644 --- a/mix.exs +++ b/mix.exs @@ -22,6 +22,11 @@ defmodule StbImage.MixProject do def application do [ + env: [ + kino_render_encoding: :png, + kino_render_max_size: {8192, 8192}, + kino_render_tab_order: [:image, :raw] + ], extra_applications: [:logger] ] end @@ -30,6 +35,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 diff --git a/mix.lock b/mix.lock index def45d8..a4241f2 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, }