%% Runs the optional albedo-render binary (native/render). It is a subprocess,
%% so a crash is an error, never a daemon loss. snapcompact renders frames
%% through it, and requests fit oversized images with it.
-module(albedo_render).
-export([run/1, scratch/1, parse_image_line/1, fit/2]).
-define(RENDER_TIMEOUT_MS, 30000).
%% The image in canonical base64, scaled so neither edge passes Edge:
%% {ok, {Mime, Base64, Width, Height, DecodedBytes}} or {error, Reason}.
fit(Base64, Edge) ->
Scratch = scratch("albedo-fit-"),
try
ok = file:make_dir(Scratch),
In = filename:join(Scratch, "in"),
ok = file:write_file(In, base64:decode(Base64)),
case run(["--fit", integer_to_list(Edge), In, "--out", Scratch]) of
{ok, Output} ->
[Line | _] = binary:split(Output, <<"\n">>),
case parse_image_line(Line) of
{ok, Path, W, H} ->
{ok, Data} = file:read_file(Path),
{ok, {mime(Path), base64:encode(Data), W, H, byte_size(Data)}};
_ -> {error, <<"unparseable renderer output">>}
end;
{error, Reason} -> {error, Reason}
end
catch _:_ ->
{error, <<"the image could not be fitted">>}
after
_ = file:del_dir_r(Scratch)
end.
mime(Path) ->
case filename:extension(Path) of
<<".jpg">> -> <<"image/jpeg">>;
_ -> <<"image/png">>
end.
%% "image PATH WxH", the line the renderer prints per image it wrote.
parse_image_line(<<"image ", Rest/binary>>) ->
Toks = binary:split(Rest, <<" ">>, [global]),
case lists:reverse(Toks) of
[Dims | RevPath] when length(RevPath) > 0 ->
Path = iolist_to_binary(lists:join(<<" ">>, lists:reverse(RevPath))),
case binary:split(Dims, <<"x">>) of
[W, H] ->
case catch {binary_to_integer(W), binary_to_integer(H)} of
{WI, HI} when is_integer(WI), is_integer(HI), WI > 0, HI > 0 ->
{ok, Path, WI, HI};
_ -> error
end;
_ -> error
end;
_ -> error
end;
parse_image_line(<<>>) -> skip;
parse_image_line(_) -> error.
%% A unique scratch dir keeps concurrent renders from colliding.
scratch(Prefix) ->
Tmp = case os:getenv("TMPDIR") of false -> "/tmp"; T -> T end,
filename:join(Tmp, Prefix ++ integer_to_list(erlang:unique_integer([positive]))).
%% The binary's stdout, stderr merged in so failures explain themselves.
run(Args) ->
case renderer() of
Bin when is_list(Bin), Bin =/= "" ->
Port = open_port({spawn_executable, Bin},
[{args, Args}, exit_status, binary, hide, stderr_to_stdout]),
drain(Port, []);
_ ->
{error, <<"albedo-render is not built; run native/render/install.sh">>}
end.
renderer() ->
Bin = case code:priv_dir(albedo) of
{error, _} -> "";
Priv -> filename:join(Priv, "bin/albedo-render")
end,
case filelib:is_file(Bin) of
true -> Bin;
false -> os:find_executable("albedo-render")
end.
drain(Port, Acc) ->
receive
{Port, {data, Data}} -> drain(Port, [Data | Acc]);
{Port, {exit_status, 0}} -> {ok, iolist_to_binary(lists:reverse(Acc))};
{Port, {exit_status, _}} ->
{error, iolist_to_binary(["renderer failed: " | lists:reverse(Acc)])};
{Port, eof} -> drain(Port, Acc)
after ?RENDER_TIMEOUT_MS ->
catch erlang:port_close(Port),
{error, <<"the renderer timed out">>}
end.