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