defmodule QRNBU.Renderer do
@moduledoc """
QR code rendering functionality for NBU payment QR codes.
This module provides functions to convert NBU QR code strings into various visual formats:
- PNG images (binary data)
- SVG graphics (XML string) with optional logo overlay
- Terminal output (Unicode blocks)
Uses the `eqrcode` library for QR code generation with configurable error correction levels.
## Examples
# Generate and render as PNG
{:ok, qr_string} = QRNBU.generate(:v001, payment_data)
{:ok, png_binary} = QRNBU.Renderer.to_png(qr_string)
File.write!("payment.png", png_binary)
# Generate and render as SVG (with UAH logo by default)
{:ok, svg_string} = QRNBU.Renderer.to_svg(qr_string)
File.write!("payment.svg", svg_string)
# Generate SVG without logo
{:ok, svg_string} = QRNBU.Renderer.to_svg(qr_string, logo: false)
# Display in terminal
QRNBU.Renderer.to_terminal(qr_string)
## Error Correction Levels
QR codes support four error correction levels (from lowest to highest):
- `:l` (L) - ~7% error recovery
- `:m` (M) - ~15% error recovery (default)
- `:q` (Q) - ~25% error recovery
- `:h` (H) - ~30% error recovery
Higher error correction levels create larger QR codes but are more resistant to damage.
When a logo is embedded (SVG only), error correction is automatically set to `:h` for
maximum damage tolerance.
## Logo Support (SVG Only)
SVG output supports embedding a logo in the center of the QR code:
- `logo: true` - Embed UAH currency sign (default)
- `logo: false` - No logo
- `logo: "/path/to/logo.svg"` - Custom SVG logo from file
- `logo: {:svg, "<svg>...</svg>"}` - Inline SVG string
PNG output does not support logo overlays.
"""
alias QRNBU.Renderer.Logo
@type qr_string :: String.t()
@type error_correction :: :l | :m | :q | :h
@type png_binary :: binary()
@type svg_string :: String.t()
@type logo_option :: Logo.logo_option()
@doc """
Renders an NBU QR code string as a PNG image.
## Parameters
- `qr_string` - The QR code string from `QRNBU.generate/2`
- `opts` - Optional keyword list:
- `:error_correction` - Error correction level (default: `:medium`)
- `:width` - Image width in pixels (default: 300)
## Returns
- `{:ok, png_binary}` - PNG image as binary data
- `{:error, reason}` - Error message
## Examples
iex> {:ok, qr} = QRNBU.generate(:v001, %{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів"
...> })
iex> {:ok, png} = QRNBU.Renderer.to_png(qr)
iex> is_binary(png) and byte_size(png) > 0
true
iex> {:ok, png} = QRNBU.Renderer.to_png("test", width: 500, error_correction: :h)
iex> is_binary(png)
true
"""
@spec to_png(qr_string(), keyword()) :: {:ok, png_binary()} | {:error, String.t()}
def to_png(qr_string, opts \\ []) when is_binary(qr_string) do
error_correction = Keyword.get(opts, :error_correction, :m)
width = Keyword.get(opts, :width, 300)
with {:ok, qr_code} <- create_qr_code(qr_string, error_correction) do
png_binary = EQRCode.png(qr_code, width: width)
{:ok, png_binary}
end
rescue
e -> {:error, "PNG rendering failed: #{Exception.message(e)}"}
end
@doc """
Renders an NBU QR code string as an SVG image.
## Parameters
- `qr_string` - The QR code string from `QRNBU.generate/2`
- `opts` - Optional keyword list:
- `:error_correction` - Error correction level (default: `:m`, auto-upgraded to `:h` when logo is enabled)
- `:width` - SVG width (default: 300)
- `:logo` - Logo option (default: `true`):
- `true` - Embed UAH currency sign
- `false` - No logo
- `"/path/to/logo.svg"` - Custom SVG logo from file
- `{:svg, "<svg>...</svg>"}` - Inline SVG string
## Returns
- `{:ok, svg_string}` - SVG XML as string
- `{:error, reason}` - Error message
## Examples
iex> {:ok, qr} = QRNBU.generate(:v001, %{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів"
...> })
iex> {:ok, svg} = QRNBU.Renderer.to_svg(qr)
iex> String.contains?(svg, "<svg")
true
iex> {:ok, svg} = QRNBU.Renderer.to_svg("test", width: 400, logo: false)
iex> String.contains?(svg, "width=\\"400.0\\"")
true
iex> {:ok, svg} = QRNBU.Renderer.to_svg("test", logo: true)
iex> String.contains?(svg, "uah-logo")
true
"""
@spec to_svg(qr_string(), keyword()) :: {:ok, svg_string()} | {:error, String.t()}
def to_svg(qr_string, opts \\ []) when is_binary(qr_string) do
logo_option = Keyword.get(opts, :logo, true)
width = Keyword.get(opts, :width, 300)
# Auto-upgrade error correction to :h when logo is enabled for better scanability
error_correction =
if logo_enabled?(logo_option) do
Keyword.get(opts, :error_correction, :h)
else
Keyword.get(opts, :error_correction, :m)
end
with {:ok, qr_code} <- create_qr_code(qr_string, error_correction),
svg_string <- EQRCode.svg(qr_code, width: width),
{:ok, svg_with_logo} <- Logo.embed(svg_string, logo_option, width) do
{:ok, svg_with_logo}
end
rescue
e -> {:error, "SVG rendering failed: #{Exception.message(e)}"}
end
@spec logo_enabled?(Logo.logo_option()) :: boolean()
defp logo_enabled?(false), do: false
defp logo_enabled?(_), do: true
@doc """
Renders an NBU QR code string to the terminal using Unicode block characters.
Displays the QR code directly in the terminal using filled and empty Unicode blocks.
## Parameters
- `qr_string` - The QR code string from `QRNBU.generate/2`
- `opts` - Optional keyword list:
- `:error_correction` - Error correction level (default: `:medium`)
## Returns
- `:ok` - Successfully printed to terminal
- `{:error, reason}` - Error message
## Examples
iex> {:ok, qr} = QRNBU.generate(:v001, %{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів"
...> })
iex> QRNBU.Renderer.to_terminal(qr)
:ok
"""
@spec to_terminal(qr_string(), keyword()) :: :ok | {:error, String.t()}
def to_terminal(qr_string, opts \\ []) when is_binary(qr_string) do
error_correction = Keyword.get(opts, :error_correction, :m)
with {:ok, qr_code} <- create_qr_code(qr_string, error_correction) do
terminal_output = EQRCode.render(qr_code)
IO.puts(terminal_output)
:ok
end
rescue
e -> {:error, "Terminal rendering failed: #{Exception.message(e)}"}
end
@doc """
Generates and saves an NBU QR code as a PNG file.
Convenience function that combines generation and PNG rendering.
## Parameters
- `version` - QR code version (`:v001`, `:v002`, or `:v003`)
- `data` - Payment data map
- `file_path` - Path to save the PNG file
- `opts` - Optional rendering options (see `to_png/2`)
## Returns
- `:ok` - File successfully saved
- `{:error, reason}` - Error message
## Examples
iex> QRNBU.Renderer.save_png(:v001, %{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів"
...> }, "/tmp/payment.png")
:ok
"""
@spec save_png(QRNBU.version(), QRNBU.qr_data(), Path.t(), keyword()) ::
:ok | {:error, String.t()}
def save_png(version, data, file_path, opts \\ []) do
with {:ok, qr_string} <- QRNBU.generate(version, data),
{:ok, png_binary} <- to_png(qr_string, opts),
:ok <- File.write(file_path, png_binary) do
:ok
else
{:error, reason} when is_binary(reason) -> {:error, reason}
{:error, reason} -> {:error, "Failed to save PNG: #{inspect(reason)}"}
end
end
@doc """
Generates and saves an NBU QR code as an SVG file.
Convenience function that combines generation and SVG rendering.
By default, embeds the UAH currency sign logo in the center.
## Parameters
- `version` - QR code version (`:v001`, `:v002`, or `:v003`)
- `data` - Payment data map
- `file_path` - Path to save the SVG file
- `opts` - Optional rendering options (see `to_svg/2`), including:
- `:logo` - Logo option (default: `true` for UAH sign)
## Returns
- `:ok` - File successfully saved
- `{:error, reason}` - Error message
## Examples
iex> QRNBU.Renderer.save_svg(:v002, %{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів",
...> amount: Decimal.new("100.50")
...> }, "/tmp/payment.svg")
:ok
"""
@spec save_svg(QRNBU.version(), QRNBU.qr_data(), Path.t(), keyword()) ::
:ok | {:error, String.t()}
def save_svg(version, data, file_path, opts \\ []) do
with {:ok, qr_string} <- QRNBU.generate(version, data),
{:ok, svg_string} <- to_svg(qr_string, opts),
:ok <- File.write(file_path, svg_string) do
:ok
else
{:error, reason} when is_binary(reason) -> {:error, reason}
{:error, reason} -> {:error, "Failed to save SVG: #{inspect(reason)}"}
end
end
@doc """
Generates and displays an NBU QR code in the terminal.
Convenience function that combines generation and terminal rendering.
## Parameters
- `version` - QR code version (`:v001`, `:v002`, or `:v003`)
- `data` - Payment data map
- `opts` - Optional rendering options (see `to_terminal/2`)
## Returns
- `:ok` - Successfully displayed
- `{:error, reason}` - Error message
## Examples
iex> QRNBU.Renderer.display(:v001, %{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів"
...> })
:ok
"""
@spec display(QRNBU.version(), QRNBU.qr_data(), keyword()) :: :ok | {:error, String.t()}
def display(version, data, opts \\ []) do
with {:ok, qr_string} <- QRNBU.generate(version, data),
:ok <- to_terminal(qr_string, opts) do
:ok
end
end
# Private helpers
@spec create_qr_code(qr_string(), error_correction()) :: {:ok, EQRCode.Matrix.t()}
defp create_qr_code(qr_string, error_correction) do
# EQRCode.encode/2 returns a matrix directly, not a tuple
qr_code = EQRCode.encode(qr_string, error_correction)
{:ok, qr_code}
end
end