234 lines
6.7 KiB
Elixir
234 lines
6.7 KiB
Elixir
defmodule ComponentsElixir.QRCode do
|
|
@moduledoc """
|
|
QR Code generation and parsing for storage locations.
|
|
|
|
Provides functionality to generate QR codes for storage locations
|
|
and parse them back to retrieve location information.
|
|
"""
|
|
|
|
@doc """
|
|
Generates a QR code data string for a storage location.
|
|
|
|
Format: SL:{level}:{qr_code}:{parent_qr_or_ROOT}
|
|
|
|
## Examples
|
|
|
|
iex> location = %StorageLocation{level: 1, qr_code: "ABC123", parent: nil}
|
|
iex> ComponentsElixir.QRCode.generate_qr_data(location)
|
|
"SL:1:ABC123:ROOT"
|
|
|
|
iex> parent = %StorageLocation{qr_code: "SHELF1"}
|
|
iex> drawer = %StorageLocation{level: 2, qr_code: "DRAW01", parent: parent}
|
|
iex> ComponentsElixir.QRCode.generate_qr_data(drawer)
|
|
"SL:2:DRAW01:SHELF1"
|
|
"""
|
|
def generate_qr_data(storage_location) do
|
|
parent_code =
|
|
case storage_location.parent do
|
|
nil -> "ROOT"
|
|
parent -> parent.qr_code
|
|
end
|
|
|
|
"SL:#{storage_location.level}:#{storage_location.qr_code}:#{parent_code}"
|
|
end
|
|
|
|
@doc """
|
|
Parses a QR code string and extracts components.
|
|
|
|
## Examples
|
|
|
|
iex> ComponentsElixir.QRCode.parse_qr_data("SL:1:ABC123:ROOT")
|
|
{:ok, %{level: 1, code: "ABC123", parent: "ROOT"}}
|
|
|
|
iex> ComponentsElixir.QRCode.parse_qr_data("invalid")
|
|
{:error, :invalid_format}
|
|
"""
|
|
def parse_qr_data(qr_string) do
|
|
case String.split(qr_string, ":") do
|
|
["SL", level_str, code, parent] ->
|
|
case Integer.parse(level_str) do
|
|
{level, ""} ->
|
|
{:ok, %{level: level, code: code, parent: parent}}
|
|
_ ->
|
|
{:error, :invalid_level}
|
|
end
|
|
_ ->
|
|
{:error, :invalid_format}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates if a string looks like a storage location QR code.
|
|
|
|
## Examples
|
|
|
|
iex> ComponentsElixir.QRCode.valid_storage_qr?("SL:1:ABC123:ROOT")
|
|
true
|
|
|
|
iex> ComponentsElixir.QRCode.valid_storage_qr?("COMP:12345")
|
|
false
|
|
"""
|
|
def valid_storage_qr?(qr_string) do
|
|
case parse_qr_data(qr_string) do
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Generates a printable label data structure for a storage location.
|
|
|
|
This could be used to generate PDF labels or send to a label printer.
|
|
"""
|
|
def generate_label_data(storage_location) do
|
|
qr_data = generate_qr_data(storage_location)
|
|
|
|
%{
|
|
qr_code: qr_data,
|
|
name: storage_location.name,
|
|
path: storage_location.path,
|
|
level: storage_location.level,
|
|
description: storage_location.description
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Generates multiple QR codes for disambiguation testing.
|
|
|
|
This is useful for testing multi-QR detection scenarios.
|
|
"""
|
|
def generate_test_codes(storage_locations) when is_list(storage_locations) do
|
|
Enum.map(storage_locations, &generate_qr_data/1)
|
|
end
|
|
|
|
@doc """
|
|
Generates a QR code image (PNG) for a storage location.
|
|
|
|
Returns the binary PNG data that can be saved to disk or served directly.
|
|
|
|
## Options
|
|
|
|
- `:size` - The size of the QR code image in pixels (default: 200)
|
|
- `:background` - Background color as `{r, g, b}` tuple (default: white)
|
|
- `:foreground` - Foreground color as `{r, g, b}` tuple (default: black)
|
|
|
|
## Examples
|
|
|
|
iex> location = %StorageLocation{level: 1, qr_code: "ABC123"}
|
|
iex> {:ok, png_data} = ComponentsElixir.QRCode.generate_qr_image(location)
|
|
iex> File.write!("/tmp/qr_code.png", png_data)
|
|
|
|
"""
|
|
def generate_qr_image(storage_location, _opts \\ []) do
|
|
qr_data = generate_qr_data(storage_location)
|
|
|
|
qr_data
|
|
|> QRCode.create()
|
|
|> QRCode.render(:png)
|
|
end
|
|
|
|
@doc """
|
|
Generates and saves a QR code image to the specified file path.
|
|
|
|
## Examples
|
|
|
|
iex> location = %StorageLocation{level: 1, qr_code: "ABC123"}
|
|
iex> ComponentsElixir.QRCode.save_qr_image(location, "/tmp/qr_code.png")
|
|
:ok
|
|
|
|
"""
|
|
def save_qr_image(storage_location, file_path, opts \\ []) do
|
|
case generate_qr_image(storage_location, opts) do
|
|
{:ok, png_data} ->
|
|
# Ensure directory exists
|
|
file_path
|
|
|> Path.dirname()
|
|
|> File.mkdir_p!()
|
|
|
|
File.write!(file_path, png_data)
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Generates a QR code image URL for serving via Phoenix static files.
|
|
|
|
This function generates the QR code image and saves it to the static directory,
|
|
returning a URL that can be used in templates.
|
|
|
|
## Examples
|
|
|
|
iex> location = %StorageLocation{id: 123, qr_code: "ABC123"}
|
|
iex> ComponentsElixir.QRCode.get_qr_image_url(location)
|
|
"/qr_codes/storage_location_123.png"
|
|
|
|
"""
|
|
def get_qr_image_url(storage_location, opts \\ []) do
|
|
filename = "storage_location_#{storage_location.id}.png"
|
|
file_path = Path.join([Application.app_dir(:components_elixir, "priv/static/user_generated/qr_codes"), filename])
|
|
|
|
# Generate and save the image if it doesn't exist or if regeneration is forced
|
|
force_regenerate = Keyword.get(opts, :force_regenerate, false)
|
|
|
|
if force_regenerate || !File.exists?(file_path) do
|
|
case save_qr_image(storage_location, file_path, opts) do
|
|
:ok -> "/user_generated/qr_codes/#{filename}"
|
|
{:error, _reason} -> nil
|
|
end
|
|
else
|
|
"/user_generated/qr_codes/#{filename}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Generates QR code images for multiple storage locations (bulk generation).
|
|
|
|
Returns a list of results indicating success or failure for each location.
|
|
|
|
## Examples
|
|
|
|
iex> locations = [location1, location2, location3]
|
|
iex> ComponentsElixir.QRCode.bulk_generate_images(locations)
|
|
[
|
|
{:ok, "/qr_codes/storage_location_1.png"},
|
|
{:ok, "/qr_codes/storage_location_2.png"},
|
|
{:error, "Failed to generate for location 3"}
|
|
]
|
|
|
|
"""
|
|
def bulk_generate_images(storage_locations, opts \\ []) do
|
|
# Use Task.async_stream for concurrent generation with back-pressure
|
|
storage_locations
|
|
|> Task.async_stream(
|
|
fn location ->
|
|
case get_qr_image_url(location, Keyword.put(opts, :force_regenerate, true)) do
|
|
nil -> {:error, "Failed to generate QR code for location #{location.id}"}
|
|
url -> {:ok, url}
|
|
end
|
|
end,
|
|
timeout: :infinity,
|
|
max_concurrency: System.schedulers_online() * 2
|
|
)
|
|
|> Enum.map(fn {:ok, result} -> result end)
|
|
end
|
|
|
|
@doc """
|
|
Cleans up QR code images for deleted storage locations.
|
|
|
|
Should be called when storage locations are deleted to prevent orphaned files.
|
|
"""
|
|
def cleanup_qr_image(storage_location_id) do
|
|
filename = "storage_location_#{storage_location_id}.png"
|
|
file_path = Path.join([Application.app_dir(:components_elixir, "priv/static/user_generated/qr_codes"), filename])
|
|
|
|
if File.exists?(file_path) do
|
|
File.rm(file_path)
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
end
|