defmodule Barlix.PNG do @moduledoc """ This module implements the PNG renderer. """ @white 255 @black 0 @doc """ Renders the given code in png image format. ## Options * `:file` - (path) - target file path. If not set, PNG will be returned as iodata. * `:xdim` - (integer) - width of a single bar in pixels. Defaults to `1`. * `:height` - (integer) - height of the bar in pixels. Defaults to `100`. * `:margin` - (integer) - margin size in pixels. Defaults to `10`. """ @spec print(Barlix.code(), Keyword.t()) :: :ok | {:ok, iodata} def print({:D1, code}, options \\ []) do xdim = Keyword.get(options, :xdim, 1) height = Keyword.get(options, :height, 100) margin = Keyword.get(options, :margin, 10) width = xdim * length(code) + margin * 2 row = row(code, xdim, margin) case Keyword.has_key?(options, :file) do true -> print_to_file(Keyword.fetch!(options, :file), row, width, height, margin) false -> print_to_memory(row, width, height, margin) end end defp row(code, xdim, margin) do margin_pixels = map_seq(margin, fn _ -> @white end) white = Enum.map(1..xdim, fn _ -> @white end) black = Enum.map(1..xdim, fn _ -> @black end) bar_pixels = Enum.map(code, fn x -> case x do 1 -> black 0 -> white end end) [margin_pixels, bar_pixels, margin_pixels] end defp print_to_file(file_path, row, width, height, margin) do file = File.open!(file_path, [:write]) write_png(row, width, height, margin, file: file) :ok = File.close(file) end defp print_to_memory(row, width, height, margin) do {:ok, storage} = start_storage() write_png(row, width, height, margin, call: &save_chunk(storage, &1)) release_storage(storage) end defp write_png(row, width, height, margin, options) do png_options = %{ size: {width, height + 2 * margin}, mode: {:grayscale, 8} } png = :png2.create(Enum.into(options, png_options)) margin_row = map_seq(width, fn _ -> @white end) append_margin_row = fn _ -> :png2.append(png, {:row, margin_row}) end _ = map_seq(margin, append_margin_row) Enum.each(1..height, fn _ -> :png2.append(png, {:row, row}) end) _ = map_seq(margin, append_margin_row) :png2.close(png) end defp map_seq(size, callback) do if size > 0, do: Enum.map(1..size, fn x -> callback.(x) end), else: [] end defp start_storage, do: Agent.start_link(fn -> [] end) defp save_chunk(storage, iodata) do Agent.update(storage, fn acc -> [acc, iodata] end) end defp release_storage(storage) do iodata = Agent.get(storage, & &1) :ok = Agent.stop(storage) {:ok, iodata} end end