Skip to main content

lib/pdf/dimension.ex

defmodule Pdf.Dimension do
  @moduledoc """
  Resolves relative dimensions against a parent reference.

  Supports:
  - `:full` — 100% of the parent dimension
  - `"N%"` — percentage of the parent dimension (e.g. `"50%"`)
  - `number` — absolute value (pass-through)

  ## Examples

      iex> Pdf.Dimension.resolve(:full, 400)
      400

      iex> Pdf.Dimension.resolve("50%", 400)
      200.0

      iex> Pdf.Dimension.resolve(200, 400)
      200
  """

  @doc """
  Resolve a single dimension value against a parent dimension.
  """
  @spec resolve(value :: number | :full | String.t(), parent_dim :: number) :: number
  def resolve(:full, parent_dim), do: parent_dim

  def resolve(pct, parent_dim) when is_binary(pct) do
    case Float.parse(String.trim_trailing(pct, "%")) do
      {n, ""} -> parent_dim * (n / 100)
      _ -> raise ArgumentError, "invalid percentage: #{inspect(pct)}"
    end
  end

  def resolve(value, _parent_dim) when is_number(value), do: value

  @doc """
  Resolve a `{w, h}` size tuple against a parent area `%{width:, height:}`.

  Returns `{resolved_w, resolved_h}`.
  """
  @spec resolve_size({any, any}, %{width: number, height: number}) :: {number, number}
  def resolve_size({w, h}, %{width: pw, height: ph}) do
    {resolve(w, pw), resolve(h, ph)}
  end

  @doc """
  Returns `true` if the value is a relative dimension (`:full` or `"N%"`).
  """
  @spec relative?(any) :: boolean
  def relative?(:full), do: true
  def relative?(v) when is_binary(v), do: String.ends_with?(v, "%")
  def relative?(_), do: false

  @doc """
  Returns `true` if either dimension in the size tuple is relative.
  """
  @spec needs_resolution?({any, any}) :: boolean
  def needs_resolution?({w, h}), do: relative?(w) or relative?(h)
end