defmodule EQRCode.PNG do @moduledoc """ Render the QR Code matrix in PNG format ```elixir qr_code_content |> EQRCode.encode() |> EQRCode.png() ``` You can specify the following attributes of the QR code: * `color`: In binary format. The default is `<<0, 0, 0>>` * `background_color`: In binary format or `:transparent`. The default is `<<255, 255, 255>>` * `width`: The width of the QR code in pixel. (the actual size may vary, due to the number of modules in the code) By default, QR code size will be dynamically generated based on the input string. """ alias EQRCode.Matrix @defaults %{ background_color: <<255, 255, 255>>, color: <<0, 0, 0>>, module_size: 11 } @transparent_alpha <<0>> @opaque_alpha <<255>> @png_signature <<137, 80, 78, 71, 13, 10, 26, 10>> @doc """ Return the PNG binary representation of the QR Code """ @spec png(Matrix.t(), map() | Keyword.t()) :: String.t() def png(%Matrix{matrix: matrix} = m, options \\ []) do matrix_size = Matrix.size(m) options = normalize_options(options, matrix_size) pixel_size = matrix_size * options[:module_size] ihdr = png_chunk("IHDR", <>) idat = png_chunk("IDAT", pixels(matrix, options)) iend = png_chunk("IEND", "") [@png_signature, ihdr, idat, iend] |> List.flatten() |> Enum.join() end defp normalize_options(options, matrix_size) do options |> Enum.into(@defaults) |> calc_module_size(matrix_size) end defp calc_module_size(%{width: width} = options, matrix_size) when is_integer(width) do size = (width / matrix_size) |> Float.round() |> trunc() Map.put(options, :module_size, size) end defp calc_module_size(options, _matrix_size), do: options defp png_chunk(type, binary) do length = byte_size(binary) crc = :erlang.crc32(type <> binary) [<>, type, binary, <>] end defp pixels(matrix, options) do matrix |> Tuple.to_list() |> Stream.map(&row_pixels(&1, options)) |> Enum.join() |> :zlib.compress() end defp row_pixels(row, %{module_size: module_size} = options) do pixels = row |> Tuple.to_list() |> Enum.map(&module_pixels(&1, options)) |> Enum.join() :binary.copy(<<0>> <> pixels, module_size) end defp module_pixels(value, %{background_color: background_color, module_size: module_size}) when is_nil(value) or value == 0 do background_color |> apply_alpha_channel() |> :binary.copy(module_size) end defp module_pixels(1, %{color: color, module_size: module_size}) do color |> apply_alpha_channel() |> :binary.copy(module_size) end defp apply_alpha_channel(:transparent), do: <<0, 0, 0>> <> @transparent_alpha defp apply_alpha_channel(color), do: color <> @opaque_alpha end